Expressr

Express.js for Ruby

Overview

Expressr brings the architecture of Express.js to Ruby. It's a minimal and flexible web application framework that couples the concepts of Express.js and Node.js with the beauty of Ruby.

Expressr runs on top of Noder (Node.js for Ruby).

Quick Start

A web app can be created and started using the following script:

require 'expressr'

app = Expressr::App.new

app.get('/hello.txt') do |request, response|
  response.out('Hello World')
end

app.listen(3000)

To start the app, put the code into a file named my_app.rb and run it:

$ ruby my_app.rb
Running Noder at 0.0.0.0:3000...

Examples

Here are some other examples of common usage:

require 'expressr'

app = Expressr::App.new

# Log every request to URLs beginning with /admin/
app.all('/admin/*') do |request, response|
  Noder.logger.info "#{request.locals.user} accessed #{request.url}"
end

# Render JSON
app.get('/some_json') do |request, response|
  response.out({ some: 'json' })
end

# Render a view
app.get('/users/:id') do (request, response)
  Noder.with ->{ User.find(request.params.id) } do |user|
    response.render('users/show', user: user.attributes)
  end
end

# Respond to a POST request by creating a record and then redirecting
app.post('/comment') do |request, response|
  Noder.with ->{ Comment.create(request.params.comment) } do |comment|
    response.redirect("/comment/#{comment.id}")
  endd
end

app.listen(3000)

See the API section below for complete documentation.

Please note that the datastore-related lines in the examples above will block the event loop as written. You'll want to use EM-Synchrony's support for whichever datastore you're using and for other IO operations.

API

Expressr::App

Expressr::App lets you create and run web apps.

Settings

An app has settings which configure it:

  • 'jsonp callback name' - The param used for determining the JSONP callback's name. The default is 'callback' (e.g. for ?callback=myFunction).
  • 'locals' - A hash of name-value pairs that will be passed to views as local variables.
  • 'root' - The root directory of the app. This is set automatically.
  • 'view engine' - The template engine used for views. The default is 'slim', and 'haml' is also supported.
  • 'views' - The path to the views within the app's root directory. The default value is 'views'.

Settings can be set using #set and retrieved using #get:

app.set('view engine', 'haml')
app.get('view engine') # "haml"

A hash of all settings can be accessed by using app.settings.

.new(server_options={})

Creates the app.

server_options

Please see Noder's docs for the options that can be passed to Noder::HTTP::Server. These include options like the server's address, port, whether HTTPS is enabled, etc.

#set(name, value)

Sets the value of a setting.

#get(name)

Gets the value of a setting.

#enable(name)

Sets the value of a setting to true.

#disable(name)

Sets the value of a setting to false.

#enabled?(name)

Returns a boolean of whether the setting is enabled or not.

#disabled?(name)

Returns a boolean of whether the setting is disabled or not.

#engine(value)

Sets the view engine. This is the equivalent of set('view engine', value). Valid values are 'slim' and 'haml'.

#param(name, &block)

Registers a listener for any request that includes the specified param. For example, in a request is made to /user/5 (with a /user/:user_id route) or /profile?user_id=5, the following will the log the user_id value:

app.param('user_id') do |request, response, continue, user_id|
  user = User.find(user_id)
  if user
    request.locals.user = user
  else
    Noder.logger.info "User not found: #{user_id}"
  end
end

#VERB(path, &block)

Registers a listener for any request that matches the VERB (e.g. get, post, put, delete) and the path.

Respond with Welcome! for GET requests to /welcome:

app.get('/welcome') do |request, response|
  response.out('Welcome!')
end

Respond with JSON for POST requests to /user/5/settings:

app.post('/user/:id/settings') do |request, response|
  response.out({
    user_id: request.params.id,
    params: request.params
  })
end

Regular expressions can also be used:

app.get(/^\/commits\/(\w+)\.\.(\w+)/) do |request, response|
  response.out({
    from: request.params[0],
    to: request.params[1]
  })
end

#all(path, &block)

This method functions just like the #VERB(path, &block) method, but it matches all HTTP verbs.

It's very useful for creating global logic for all requests or for requests to specific paths:

app.all('/admin/*') do |request, response|
  if !is_admin?
    response.status = 403
    response.out('Not authorized.')
  end
end

#route(path)

Returns an instance of a single route which can then be used to handle HTTP verbs with optional middleware. Using #route(path) is a recommended approach to avoiding duplicate route naming and thus typo errors.

app.route('/users').
all do |request, response|
  Noder.logger.info "Users request performed"
end.
get do |request, response|
  response.out(User.find(request.params.user_id))
end.
post do |request, response|
  user = User.create(request.params.user)
  response.out(user.attributes)
end

#locals

Application local variables are provided to all templates rendered within the application. This is useful for providing helper functions to templates, as well as app-level data.

app.locals.site_name = 'My Site'
app.locals.contact_email = '[email protected]'

#listen(port=nil, address=nil, &block)

Bind and listen for connections on the given host and port. This method is identical to Noder's Noder::HTTP::Server#listen.

app = Expressr::App.new
app.get('/hello.txt') do |request, response|
  response.out('Hello World')
end
app.listen(3000)

A block which will be called for all requests can be passed to it, too:

app = Expressr::App.new
app.listen do |request, response|
  response.out('Hello World')
end

#close

Stops the app. This is the same as Noder's Noder::HTTP::Server#close and is called when an INT or TERM signal is sent to a running server's process (e.g. when Control-C is pressed).

#settings

A hash of the app's settings:

app.set('my setting', 'My value')
value = app.settings['my setting']

Expressr::Request

Expressr::Request inherits from (and thus also includes methods from) Noder::HTTP::Request.

#params

Similar to Rails' params, this includes params from the query string, POST data, and route parameters. Params can be accessed in three ways: params.user_id, params[:user_id], or params['user_id'].

For example, a request to /user/3?comment_id=4 will include two params:

app.get('/user/:user_id') do |request, response|
  response.out({
    user_id: request.params.user_id,
    comment_id: request.params.comment_id
  })
end

If a regex route is used, the matches can be accessed at their integer indexes:

app.get(/\/user\/(\d+)\/comment\/(\d+)/) do |request, response|
  response.out({
    user_id: request.params[0],
    comment_id: request.params[1]
  })
end

#query

Similar to #params, but it only includes params from the query string.

For example, a request to /user/3?comment_id=4 will only include comment_id:

app.get('/user/:user_id') do |request, response|
  response.out({
    comment_id: request.query.comment_id
  })
end

#param(name)

Returns the value of param name when present. This is the equivalent of params.name or params[name], which are the preferred forms.

request.param('user_id')

#get(name)

Returns the value of the name header when present.

request.get('Content-Type') # "text/plain"
request.get('Something') # nil

Aliased as #header(name).

#accepts(types)

Check if the given types are acceptable, returning the best match when true, otherwise nil.

# Accept: text/*, application/json
request.accepts('text/html') # "text/html"
request.accepts('image/png') # nil

#is?(type)

Check if the given types are acceptable, returning the best match when true, otherwise nil.

# Content-Type: text/html; charset=utf-8
request.is('text/html') # true
request.is('image/png') # false

#ip

Returns the remote IP address.

request.ip # "68.1.8.45"

#path

Returns the path of the requested URL.

# example.com/users?sort=desc
request.path # "/users"

#host

Returns the hostname from the "Host" header field (without the port).

# Host: "example.com:3000"
request.host # "example.com"

#xhr?

Check whether the request was issued with the "X-Requested-With" header field set to "XMLHttpRequest" (jQuery etc).

# Host: "example.com:3000"
request.xhr? # false

#protocol

Returns the protocol string of the request (e.g. 'http', 'https').

# "http://example.com/"
request.protocol # 'http'

#secure?

Checks whether a TLS connection is established. This is the equivalent of request.protocol == 'https'.

# "http://example.com/"
request.secure? # false

#subdomains

Returns the subdomains as an array

# Host: "tobi.ferrets.example.com"
request.subdomains # ["ferrets", "tobi"]

#original_url

This is similar to #url, except that it retains the original URL, allowing you to rewrite #url freely.

# /search?q=something
request.original_url # "/search?q=something"

Expressr::Response

Expressr::Response inherits from (and thus also includes methods from) Noder::HTTP::Response.

#set(name, value=nil)

Set a header's value, or pass a hash as a single argument to set multiple headers at once.

response.set('Content-Type', 'text/plain')
response.set({
  'Content-Type' => 'text/plain',
  'Content-Length' => '123',
  'ETag' => '12345'
})

#get(name)

Returns a header's value.

response.get('Content-Type') # "text/plain"

Sets a cookie. All of the options supported by CGI::Cookie are supported.

response.cookie('user_id', '15')
response.cookie('remember_me', '1', {
  'expires' => Time.now + 14.days,
  'domain' => 'example.com'
})

Sets a cookie. All of the options supported by CGI::Cookie are supported.

response.cookie('user_id', '15')
response.clear_cookie('user_id')

#redirect(status_or_url, url=nil)

Redirects to the specified URL with an optional status (default is 302).

response.redirect('/foo/bar')
response.redirect(303, '/foo/bar')
response.redirect('https://www.google.com')

#location(url)

Sets the Location header's value.

response.location('/foo/bar')
response.location('https://www.google.com')

#out(status_or_content=nil, content=nil)

Sends the response. (This is the equivalent of Express.js's send method, which has another use in Ruby.)

response.out({ some: 'json' })
response.out('some html')
response.out(404, 'Sorry, we cannot find that!')
response.out(500, { error: 'something blew up' })
response.out(200)

When the content is a string, the Content-Type is set to text/html.

When the content is a hash, the hash is converted to JSON and the Content-Type is set to application/json.

#json(status_or_content=nil, content=nil)

Sends a JSON response. This identical to #out when an array or object is passed.

response.json({ some: 'json' })
response.json(500, { error: 'something blew up' })

#jsonp(status_or_content=nil, content=nil)

Sends a JSONP response. This identical to #json, but it provides JSONP support if the request specifies a JSONP callback.

# /?callback=foo
response.jsonp({ some: 'json' }) # "foo({"some":"json"});"
# /
response.jsonp({ some: 'json' }) # "{"some":"json"}"

The JSONP callback name defaults to 'callback', but it can be set using the app's settings:

app.settings.set('jsonp callback name', 'cb')
# /?cb=foo
response.jsonp({ some: 'json' }) # "foo({"some":"json"});"

#type(value)

Sets the Content-Type header to the value.

response.type('html')
response.type('application/json')

#format(hash)

Performs content-negotiation on the request Accept header field when present.

response.format({
  'text/html' => proc { |request, response|
    response.out("<h3>Some HTML</h3>")
  },
  'text/plain' => proc { |request, response|
      response.out("Some text")
  },
  'application/json' => proc { |request, response|
    response.out({ some: json })
  },
})

Content type synonyms are supported (e.g. 'json' and 'application/json' are equivalent):

response.format({
  'html' => proc { |request, response|
    response.out("<h3>Some HTML</h3>")
  },
  'text' => proc { |request, response|
      response.out("Some text")
  },
  'json' => proc { |request, response|
    response.out({ some: json })
  },
})

#attachment(filename=nil)

Sets the Content-Disposition header field to "attachment". If a filename is given then the Content-Type will be automatically set based on the extension via #type, and the Content-Disposition's "filename=" parameter will be set.

response.attachment
# Content-Disposition: attachment

response.attachment('path/to/logo.png')
# Content-Disposition: attachment; filename="logo.png"
# Content-Type: image/png

#send_file(path, options={})

Transfers the file at the given path.

Automatically defaults the Content-Type response header field based on the filename's extension.

options
  • root - Root directory for relative filenames
app.get('/user/:uid/photos/:file') do |request, response|
  uid = request.params.uid
  file = request.params.file

  if request.locals.user.may_view_files_from(uid)
    response.sendfile("/uploads/#{uid}/#{file}")
  else
    response.out(403, "Sorry! You can't see that.")
  end
end

#download(path, filename=nil)

Transfer the file at path as an "attachment". Typically browsers will prompt the user for download. The Content-Disposition "filename=" parameter (the one that will appear in the brower dialog is set to path by default), but you can also provide an override filename.

response.download('/report-12345.pdf')
response.download('/report-12345.pdf', 'report.pdf')

Join the given links to populate the "Link" response header field.

response.links({
  next: 'http://api.example.com/users?page=2',
  last: 'http://api.example.com/users?page=5'
})

# Link: <http://api.example.com/users?page=2>; rel="next",
#       <http://api.example.com/users?page=5>; rel="last"

#locals

Response local variables are scoped to the request, thus only available to the view(s) rendered during that request / response cycle, if any.

This object is useful for exposing request-level information such as the request pathname, authenticated user, user settings, etc.

app.use do (request, response)
  response.locals.user = User.find(request.params.user_id)
  response.locals.authenticated = !response.locals.user.is_anonymous?
end

#render(view, locals=nil, &block)

Renders a view. The view's local variables are supplied by both the locals argument and the app's locals setting. If the rendering raises an exception, the &block is called with the exception as an argument.

app.get('/users/:id') do (request, response)
  user = User.find(request.params.id)
  response.render('profile', user: user.attributes)
end

app.get('/contact') do (request, response)
  response.render('contact') do |exception|
    response.render('error')
  end
end

To set the locals that will be passed to all views, use:

app.locals.site_name = 'My Site'
app.locals.contact_email = '[email protected]'

By default, Expressr looks for views in the views directory (e.g. views/profile.slim).

To set the directory of the views, use:

app.set('views', File.expand_path('../my_views', __FILE__))

Expressr uses the Slim template engine by default, but it also supports Haml:

app.set('view engine', 'haml')

If you'd like to add support for other template engines, doing so is fairly straightforward; just grep for 'slim' in the codebase, and the steps needed to support a new engine should be clear.

Expressr::Router

Expressr::App lets you create routes for your app. An app's router can be accessed at app.router.

app = Expressr::App.new
app.router.get('/hello.txt') do |request, response|
  response.out('Hello World')
end

#use(path=nil, &block)

Please see the documentation for Expressr::App#use, which has the same behavior.

#param(name, &block)

Please see the documentation for Expressr::App#use, which has the same behavior.

#use(path=nil, &block)

Please see the documentation for Expressr::App#use, which has the same behavior.

#VERB(path, &block)

Please see the documentation for Expressr::App#use, which has the same behavior.

#use(path=nil, &block)

Please see the documentation for Expressr::App#use, which has the same behavior.

#use(path=nil, &block)

Please see the documentation for Expressr::App#use, which has the same behavior.

#use(path=nil, &block)

Please see the documentation for Expressr::App#use, which has the same behavior.

License

Expressr is released under the MIT License. Please see the MIT-LICENSE file for details.