Express.js for Ruby
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
spec
.gitignore
Gemfile
MIT-LICENSE
README.md
expressr.gemspec

README.md

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 Noder (Node.js for Ruby), which runs on EventMachine.

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 = 'contact@mysite.com'

#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"

#cookie(name, value, options={})

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'
})

#clear_cookie(name, value, options={})

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')

#links(links)

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 = 'contact@mysite.com'

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.

Notes

Expressr is not currently a full implementation of Express.js, and some of its underlying architecture differs from Express.js's. If you see any places where it could be improved or added to, absolutely feel free to submit a PR.

License

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