router.get '/' .to 'Main.index' .as 'home' router.resource 'Posts' .where id: /\d+/ .member -> @resource 'Comments' .where id:/[abc]+/ .collection -> @get '/popular(.:format)' .to controller: 'Comments' action: 'popular' .as 'popular_comments'

Barista

A simple, powerful router
for node.js and the browser

Github Npm

Overview

Barista is a URL router & generator, designed to help you decouple routing from your framework's application logic. Its function can be roughly split into two parts: parsing URLs into actionable parameters, and generating URLs from parameters.

Barista is not concerned with dispatching requests, nor does it have any built-in concept of 404s - although it's trivial to wire up appropriate actions in each case.

You can find it in the wild as the primary Geddy router.

Install

You can use Barista in either node JS or in the browser.

Node JS

Install via npm, thusly:

npm install --save barista

Then require & instantiate like so:

var Router = require('barista').Router
  , router = new Router;
{ Router } = require 'barista'
router = new Router

Use it in the browser

Install via bower, thusly:

bower install --save barista

Instantiate a router like so, then use it as you normally would!

var router = new Barista;
router = new Barista

Defining Routes

A simple example

// a GET request to '/products'
router.match( '/products', 'GET' )
      .to( 'Products.index' )
            // =>
{
  controller: 'Products',
  action: 'index',
  method: 'GET'
}
# a GET request to '/products'
router
.match '/products', 'GET'
.to 'Products.index'
            # =>
{
  controller: 'Products'
  action: 'index'
  method: 'GET'
}

Route keys

try it
              router.match( '/products/:id', 'GET' )
      .to( 'Products.show' )

router.match( '/profiles/:username', 'GET' )
      .to( 'Users.show' )

// things enclosed in parens are optional
router.match( '/products/:id(.:format)', 'GET' )
      .to( 'Products.show' )

// optional segments are also nestable
router.match( '/:controller(/:action(/:id))(.:format)', 'GET' )
try it
              router
.match '/products/:id', 'GET'
.to 'Products.show'

router
.match '/profiles/:username', 'GET'
.to 'Users.show'

# things enclosed in parens are optional
router
.match '/products/:id(.:format)', 'GET'
.to 'Products.show'

# optional segments are also nestable
router
.match '/:controller(/:action(/:id))(.:format)', 'GET'

Globs

Globs are identical to keys, with the exception that they also capture slashes. This can be useful for capturing values like timezone strings or setting up a catch-all route.

try it
                  router.match( '/timezones/*tzname', 'GET')
      .to( 'Timezones.select' )

                  router.first( '/timezones/America/Toronto', 'GET' )

// =>
{
  controller: 'Timezones',
  action: 'select',
  tzname: 'America/Toronto',
  method: 'GET'
}
try it
                  router
.match '/timezones/*tzname', 'GET'
.to 'Timezones.select'

                  router.first '/timezones/America/Toronto', 'GET'

# =>
{
  controller: 'Timezones'
  action: 'select'
  tzname: 'America/Toronto'
  method: 'GET'
}
try it
                  router.match( '*path(.:format)' )
      .to( 'Errors.notFound' );

                  router.first( '/somewhere/that/404s.html', 'GET' );

// =>
{
  controller: 'Errors',
  action: 'notFound',
  path: '/somewhere/that/404s',
  method: 'GET',
  format: 'html'
}
try it
                  router
.match '*path(.:format)'
.to 'Errors.notFound'

                  router.first '/somewhere/that/404s.html', 'GET'

# =>
{
  controller: 'Errors'
  action: 'notFound'
  path: '/somewhere/that/404s'
  method: 'GET'
  format: 'html'
}

Convenience methods

try it
                  // equivalent to router.match( '/users/:id', 'GET' )
router.get( '/users/:id' )
      .to( 'Users.show' )
try it
                  router
# equivalent to .match '/users/:id', 'GET'
.get '/users/:id'
.to 'Users.show'
try it
                  // equivalent to router.match( '/users/:id', 'POST' )
router.post( '/users/:id' )
      .to( 'Users.create' )
try it
                  router
# equivalent to .match '/users/:id', 'POST'
.post '/users/:id'
.to 'Users.create'
try it
                  // equivalent to router.match( '/users/:id', 'PUT' )
router.put( '/users/:id' )
      .to( 'Users.update' )
try it
                  router
# equivalent to .match '/users/:id', 'PUT'
.put '/users/:id'
.to 'Users.update'
try it
                  // equivalent to router.match( '/users/:id', 'PATCH' )
router.patch( '/users/:id' )
      .to( 'Users.update' )
try it
                  router
# equivalent to .match '/users/:id', 'PATCH'
.patch '/users/:id'
.to 'Users.update'
try it
                  // equivalent to router.match( '/users/:id', 'DELETE' )
router.del( '/users/:id' )
      .to( 'Users.destroy' )
try it
                  router
# equivalent to .match '/users/:id', 'DELETE'
.del '/users/:id'
.to 'Users.destroy'
try it
                  // equivalent to router.match( '/users/:id', 'OPTIONS' )
router.options( '/users/:id' )
      .to( 'Users.options' )
try it
                  router
# equivalent to .match '/users/:id', 'OPTIONS'
.options '/users/:id'
.to 'Users.options'

Match conditions

try it
              router.get( '/:beverage/near/:zipcode' )
      .to( 'beverage.byZipCode' )
      .where({
        // an array of options
        beverage: [ 'coffee', 'tea', 'beer', 'warm_sake' ],
        // a regex pattern
        zipcode: /\d{5}(-\d{4})?/
      })
try it
              router
.get '/:beverage/near/:zipcode'
.to 'beverage.byZipCode'
.where
  # an array of options
  beverage: [
    'coffee'
    'tea'
    'beer'
    'warm_sake'
  ]
  # a regex pattern
  zipcode: /\d{5}(-\d{4})?/

Default parameters

You can attach arbitrary default params to a route by passing a hash of defaults to the .to method (as a second argument).

Anything defined in your default params will be present in the output params. In the even you have a key in your route definition with the same name, the default param will be overridden by the one in the URL.

Default params are useful for providing fallback values for optional keys (:format, for instance), or for inferring implicit values based on a static part of the route (often used for language-specific routes).

try it
                  router.get('/comments/:id(.:format)')
      .to('comments.show', { format: 'pdf' } )
                  router.first('/comments/5','GET')

// =>
{
  controller: 'comments',
  action: 'show',
  id: 5,
  format: 'pdf'
}

router.first('/comments/5.html','GET')

// =>
{
  controller: 'comments',
  action: 'show',
  id: 5,
  format: 'html'
}
try it
                  router
.get '/comments/:id(.:format)'
.to 'comments.show', format: 'pdf'
                  router.first '/comments/5','GET'

# =>
{
  controller: 'comments'
  action: 'show'
  id: 5
  format: 'pdf'
}

router.first '/comments/5.html', 'GET'

# =>
{
  controller: 'comments'
  action: 'show'
  id: 5
  format: 'html'
}
try it
                  // english
router.get('/comments/:id')
      .to('comments.show', { lang: 'en' } )

// français
router.get('/commentaires/:id')
      .to('comments.show', { lang: 'fr' } )
                  // english
router.first('/comments/5', 'GET')

// =>
{
  controller: 'comments',
  action: 'show',
  id: 5,
  lang: 'en'
}

// français
router.first('/commentaires/5', 'GET')

// =>
{
  controller: 'comments',
  action: 'show',
  id: 5,
  lang: 'fr'
}
try it
                  # english
router
.get '/comments/:id'
.to 'comments.show', lang: 'en'

# français
router
.get '/commentaires/:id'
.to 'comments.show', lang: 'fr'
                  # english
router.first '/comments/5', 'GET'

# =>
{
  controller: 'comments'
  action: 'show'
  id: 5
  lang: 'en'
}

# français
router.first '/commentaires/5', 'GET'

# =>
{
  controller: 'comments'
  action: 'show'
  id: 5
  lang: 'fr'
}

Route names

TODO: Document this properly

Basically, there's a .as 'name' method that names the route. This is currently only useful for inspecting or deleting routes at runtime, but will make sense in the context of named generators, which are coming in v0.6.0

Feel free to prod me via this issue on GH if this is a feature you need sooner rather than later.

Nested routes

Routes can be nested under other routes or resources (more on resources below).

Nested routes are indentical to non-nested routes with two exceptions: They build off the URL of the parent, and they also inherit upstream default params and match conditions.

Nesting under a route is accomplished by calling .nest( router ) The router argument is optional, since the route being nested is aliased to this

try it
              router.get( '/posts' )
      .to( 'Posts.index' )
      .nest( function(){
        this.get( '/:id' )
            .to( 'Posts.show' )
            .nest( function(){
              this.get( '/comments' )
                  .to( 'Comments.index' )
            })
      })
try it
              router
.get '/posts'
.to 'Posts.index'
.nest ->
  @get '/:id'
  .to 'Posts.show'
  .nest ->
    @get '/comments'
    .to 'Comments.index'

As mentioned earlier, nested routes inherit upstream default params and match conditions. In most cases this is the desired outcome, but you can override these inherited peoperties by re-defining them on the nested route. Changes you make at this level will not affect the parent route or any sibling routes, but will be inherited in turn by any routes nested underneath.

try it
                  router.get( '/products/:id' )
      .to( 'Products.show', { display: 'main' } )
      .nest( function(){
        this.get( '/reviews' )
            .to( 'Reviews.index', { display: 'alt' } )
      })
try it
                  router
.get '/posts/:id'
.to 'Posts.show', display: 'main'
.nest ->
  @get '/comments'
  .to 'Comments.index', display: 'alt'
try it
                  router.get( '/posts/:id(.:format)' )
      .to( 'Posts.show' )
      .where({ // these conditions will apply to all sub-routes as well
        format: [
          'html',
          'pdf',
          'json'
        ]
      })
      .nest( function(){
        this.get( '/comments(.:format)' )
            .to( 'Comments.index' )
      })
try it
                  router
.get '/posts/:id(.:format)'
.to 'Posts.show'
.where # these conditions will apply to all sub-routes as well
  format: [
    'html'
    'pdf'
    'json'
  ]
.nest ->
  @get '/comments(.:format)'
  .to 'Comments.index'

Defining Resources

Resources are a convenient shorthand for describing REST-ful endpoints.

A simple example

try it
            // a Posts resource
router.resource( 'Posts' )

            // is equivalent to
router.get( '/posts(.:format)' )
      .to( 'Posts.index' )

router.get( '/posts/add(.:format)' )
      .to( 'Posts.add' )

router.get( '/posts/:id(.:format)' )
      .to( 'Posts.show' )

router.get( '/posts/:id/edit(.:format)' )
      .to( 'Posts.edit' )

router.post( '/posts(.:format)' )
      .to( 'Posts.create' )

router.put( '/posts/:id(.:format)' )
      .to( 'Posts.update' )

router.del( '/posts/:id(.:format)' )
      .to( 'Posts.destroy' )
try it
            # a Posts resource
router.resource 'Posts'

            # is equivalent to
router
.get  '/posts(.:format)'
.to 'Posts.index'

router
.get  '/posts/add(.:format)'
.to 'Posts.add'

router
.get  '/posts/:id(.:format)'
.to 'Posts.show'

router
.get  '/posts/:id/edit(.:format)'
.to 'Posts.edit'

router
.post '/posts(.:format)'
.to 'Posts.create'

router
.put  '/posts/:id(.:format)'
.to 'Posts.update'

router
.del  '/posts/:id(.:format)'
.to 'Posts.destroy'

Resource nesting

A resource is simply collection of routes, and as such can be defined at the root or nested under any other route. Nesting within a resource is a little less straight-forward, but makes sense when you think about what you're doing.

The two reasonably nestable routes in a resource are the collection route (i.e. '/posts' ) and the member route (i.e. '/posts/123' ).

This should be made clear by the examples below.

try it
                  router.resource( 'Posts' )
      .where({
        id: /\d+/
      })
      // nest on the collection route
      .collection( function(){
        this.get( '/recent(.:format)' )
        .to( 'Posts.recent' )
      })
try it
                  router.resource 'Posts'
      .where
        id: /\d+/
      # nest on the collection route
      .collection ->
        @get '/recent(.:format)'
        .to 'Posts.recent'

try it
                  router.resource( 'Posts' )
      .where({
        id: /\d+/
      })
      // nest on the member route
      .collection( function(){
        this.get( '/like(.:format)' )
        .to( 'Posts.like' )
      })
try it
                  router.resource 'Posts'
      .where
        id: /\d+/
      # nest on the member route
      .member ->
        @get '/like(.:format)'
        .to 'Posts.like'

try it
                  router.resource( 'Posts' )
      .where({
        id: /\d+/
      })
      // nest on the member route
      .member( function(){
        this.resource( 'Comments' )
      })
try it
                  router.resource 'Posts'
      .where
        id: /\d+/
      # nest on the member route
      .member ->
        @resource 'Comments'

Handling key collisions

TODO: Document this properly

Basically, Barista automatically renames previous keys when identical nested keys are likely.

For example, whe you nest resources like this: Posts > Comments the post resource's id will be renamed to :post_id to avoid a collision with the comment resource's :id attribute. Check out the "Resources in resources" tab in the above example to see what I'm talking about.

I'm not 100% satisfied with the current implementation and plan to give it another pass before v1.0.0

Parsing URLs

If you're using Barista in your framework or "off-grid" in a custom project, you'll need to parse URLs at some point in rder to route and dispatch requests. There are two methods for doing this: router.first and router.all , though you'll almost always use the former.

router.first

This method takes a URL and an optional HTTP method, returning the resolved params if there's a matching route.

Check the annotated source for more info.

// assuming the following route definition:
router.resource( 'Posts' )

// then the URL resolves like so:
router.first( '/posts/123.json', 'GET' )

              {
  method:     "GET",
  controller: "Posts",
  action:     "show",
  id:         "123",
  format:     "json"
}
# assuming the following route definition:
router.resource 'Posts'

# then the URL resolves like so:
router.first '/posts/123.json', 'GET'

              {
  method:     "GET"
  controller: "Posts"
  action:     "show"
  id:         "123"
  format:     "json"
}

router.all

This method is almost identical to router.first , with the exception that it returns an array of all possible matches.

It's almost never useful IRL, but I've used it.

// assuming the following route definition:
router.resource( 'Posts' )

// this URL (without a method) could match the following params:
router.all( '/posts/123.json' )

              [
  {
    method:     "GET",
    controller: "Posts",
    action:     "show",
    id:         "123",
    format:     "json"
  },
  {
    method:     "PUT",
    controller: "Posts",
    action:     "show",
    id:         "123",
    format:     "json"
  },
  {
    method:     "DELETE",
    controller: "Posts",
    action:     "show",
    id:         "123",
    format:     "json"
  }
]
# assuming the following route definition:
router.resource 'Posts'

# this URL (without a method) could match the following params:
router.all '/posts/123.json'

              [
  method:     "GET"
  controller: "Posts"
  action:     "show"
  id:         "123"
  format:     "json"
,
  method:     "PUT"
  controller: "Posts"
  action:     "show"
  id:         "123"
  format:     "json"
,
  method:     "DELETE"
  controller: "Posts"
  action:     "show"
  id:         "123"
  format:     "json"
]

Generating URLs

URLs can be generated using the .url( params ) method

router.url

This essentially does the reverse of router.first , that is, it generates URLs from params objects.

An optional second boolean params will add query params for any un-resolved keys.

// assuming the following route definition:
router.resource( 'Posts' )

// then generate the URL thusly:
router.url({
  method:     "GET"
  controller: "Posts"
  action:     "show"
  id:         "123"
  format:     "json"
})

                  "/posts/123.json"
# assuming the following route definition:
router.resource 'Posts'

# then generate the URL thusly:
router.url
  method:     "GET"
  controller: "Posts"
  action:     "show"
  id:         "123"
  format:     "json"

                  "/posts/123.json"
// assuming the following route definition:
router.resource 'Posts'

// then generate the URL thusly:
params = {
  method:     "GET",
  controller: "Posts",
  action:     "show",
  id:         "123",
  format:     "json",
  love:       "cheese"
}

router.url( params, true )

                  "/posts/123.json?love=cheese"
# assuming the following route definition:
router.resource 'Posts'

# then generate the URL thusly:
params =
  method:     "GET"
  controller: "Posts"
  action:     "show"
  id:         "123"
  format:     "json"
  love:       "cheese"

router.url params, true

                  "/posts/123.json?love=cheese"

Named generators

TODO: Finish this feature

This feature is due to drop in v0.6.0

The curren plan is to make named URL generators available that have default params for the controller, action, method, and any other route defaults. Only missing attributes like :id (etc) will need to be passed in, allowing POJO models objects to be provided un-molested. this issue on GH if this is a feature you need sooner rather than later.

TODOs

Things I forgot...

Shit's broken!

Shit happens.

Write a test that fails and add it to the tests folder, then create an issue!

Patches welcome :-)

Who tf are you?

I'm Kieran Huggins in Toronto, Canada.