shelf_route 0.14.3

Router for Dart Shelf

Build Status Pub Version

Introduction

Provides Shelf middleware for defining routes.

shelf_route is a powerful router that makes it easy to define routes in a modular way.

shelf_route is designed to be:

  • easy to use out of the box
  • simple to customise
  • highly extensible to make it easy for component authors to create new routing apis and incorporate routing into their components.

This makes it very versatile and lets you mix and match it with your other favourite Shelf middleware components.

###Routing Choices###

There are a number of choices for routing in the shelf world. Here is a simple guide to help you choose between a few of them.

  1. shelf_route. Good choice if: you want a powerful router with a fluent api you don't want to use mirrors or annotations * you prefer a bit more boilerplate over any magic that comes with mirrors
  2. shelf_rest. Good choice if: you want all the features of shelf_route plus you are happy to use annotations (supported by mirrors) to significantly reduce boilerplate * you like consistency in your REST APIs and like support to help with that
  3. mojito. Good choice if: you want all the features of shelf_rest plus you want a light framework that provides a fluent api on many other shelf components for things like: authentication & authorisation; serving static resources via the filesystem or via pub serve; oauth; logging and more

In short, if you want to build your own stack then shelf_route and shelf_rest will likely suit you better. If you want a more fully featured framework, whilst still being highly extensible, then mojito is the better option.

To get a good overview of the options you have, read the blog post Routing Options in Shelf.

Using

Basics

You create a router using the router function

var myRouter = router();

Use the router's get method to add a route using the GET Http method

myRouter.get('/', (_) => new Response.ok("Hello World");

Use the router's handler property to obtain a Shelf Handler

var handler = myRouter.handler;

Now you can serve up your routes with Shelf IO

io.serve(handler, 'localhost', 8080);

So the complete hello world looks like

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_route/shelf_route.dart';

void main() {
  var myRouter = router()
      ..get('/', (_) => new Response.ok("Hello World"));

  io.serve(myRouter.handler, 'localhost', 8080);
}

Http Methods

It supports all the standard http methods

myRouter..get('/', (_) => new Response.ok("Hello World"))
        ..post('/', (_) => new Response.ok("Hello World"))
        ..put('/', (_) => new Response.ok("Hello World"))
        ..delete('/', (_) => new Response.ok("Hello World"));

You can specify several methods by using add

myRouter.add('/', ['GET', 'PUT'], (_) => new Response.ok("Hello World"));

Path Parameters

Shelf Route uses UriPattern to define the paths to match on for each route. This means you can use whatever format for the paths that you like as long as it implements this interface.

By default it uses UriTemplate which implements the powerful standard of the same name.

UriTemplate allows binding to both:

  • path segments like /greeting/fred**
  • query parameters like /greeting?name=fred**

It uses {parameter name} notation to denote path parameters.

myRouter.get('/{name}', (request) =>
          new Response.ok("Hello ${getPathParameter(request, 'name')}"));

Path parameters are fetched via Shelf Path's getPathParameter function.

Similarly you can also bind to query parameters

myRouter.get('/{name}{?age}', myHandler);

myHandler(request) {
  var name = getPathParameter(request, 'name');
  var age = getPathParameter(request, 'age');
  return new Response.ok("Hello $name of age $age");
}

Hierarchical Routers

To improve modularity you can break up your routes into a series of nested routes.

You add child routes using the addAll method.

For example you can add a child router for all routes starting with /banking

var rootRouter = 
  router()..addAll((Router r) => r
    ..addAll((Router r) => r
        ..get('/', fetchAccountHandler)
        ..post('/deposit', makeDepositHandler),
      path: '/account/{accountNumber}'),
    path: '/banking');

Then serve up all the routes via the rootRouter

io.serve(rootRouter.handler, 'localhost', 8080)

Note in this case the full path of the deposit resource is actually

/banking/account/{accountNumber}/deposit

To try this out, fire up the server and do

curl -d 'lots of money' http://localhost:8080/banking/account/1235/deposit

Route Specific Middleware

You can add additional middleware to individual routes

myRouter.get('/', (_) => new Response.ok("Hello World"), middleware: logRequests());

This middleware will be applied to all requests on that route.

If you add it to a child router it will apply to all routes for that router

var bankingRouter = rootRouter.addAll((Router r) {...},
      path: '/banking', middleware: logRequests()),

will apply to all banking routes and all sub routes of '/banking'.

Grouping Routes in Classes

The Router's addAll method takes a typedef that looks like

typedef RouteableFunction(Router router);

Thanks to Darts function emulator capability, this means you can easily group a set of routes together in a class.

class MyGroup  {
  void call(Router router) {
    router..get('/', (_) => new Response.ok("Hello World"))
          ..get('/greeting/{name}', (request) =>
              new Response.ok("Hello ${getPathParameter(request, 'name')}"));
  }
}

To make that a little more explicit you can extend the Routeable class, which simply lets you call the method createRoutes rather than call.

And since you now have a class you may as well spin off the handlers into methods

class MyGroup extends Routeable {
  void createRoutes(Router router) {
    router..get('/', helloWorld)
          ..get('/greeting/{name}', greeting);
  }
  
  Response helloWorld(request) => 
	  new Response.ok("Hello World"))

  Response greeting(request) =>
      new Response.ok("Hello ${getPathParameter(request, 'name')}"));
}

Then add it to another router

rootRouter.addAll(new MyGroup());

Printing Routes

It's easy to see all the routes defined for a router using the printRoutes function.

var router = r.router()
  ..get('/', (_) => new Response.ok("Hello World"))
  ..post('/', (_) => new Response.ok("Hello World"))
  ..get('/greeting/{name}{?age}', (request) {
    var name = getPathParameter(request, 'name');
    var age = getPathParameter(request, 'age');
    return new Response.ok("Hello $name of age $age");
  });
  
printRoutes(router);

prints

GET     ->      /
POST  ->      /
GET     ->      /greeting/{name}{?age}

Examples

See more detailed examples in the example folder under the project source.

Customising

This section deals with basic customisations.

These are powerful ways to customise how the routing works and will handle most cases for customising shelf_route.

If you need more then see the section below on Extending

Custom Path Formats

The path arguments of all the router methods accept either:

  • a String or
  • a UriPattern

By default String value will be parsed into a UriParser which means it is expected to conform to UriTemplate.

You can also implement your own UriPattern and use that instead. For example you may prefer the : style of path variables (e.g. :name).

In addition it allows you to create uri path definitions and potentially share between client and server. e.g.

var accountPattern = new UriParser(new UriTemplate('/account/{accountNumber}'));

You can now use this when you define a route and on the client.

myRouter.get(accountPattern, (_) => new Reponse.ok("Hello World"));

Installing a Custom Path Adapter

To make it more seamless to use your own path style you can install a path adapter into the router. This will be used by all routes in this router and any child routers unless you override it somewhere.

Install the adapter by passing it to the router function.

var colonStyleAdapter = ....; // obtain the adapter from somewhere

var myRouter = router(pathAdapter: colonStyleAdapter);

Now you can use colon style path parameters

myRouter.get('/:name', (request) =>
          new Response.ok("Hello ${getPathParameter(request, 'name')}"));

Custom Handler Adapters

You can install a custom handler adapter, which allows you to transform the handlers passed into the Router's methods. This allows for more seamless integration with other Shelf packages.

For example if you want to use ordinary Dart functions as handlers you can use a package like Shelf Bind. Shelf Bind provides such an adapter out of the box.

Install the adapter by passing it to the router function.

import 'package:shelf_bind/shelf_bind.dart' as bind;

var myRouter = router(handlerAdapter: bind.handlerAdapter())

Now you can do

myRouter.get('/{name}', (name) => "Hello ${name}");

instead of

myRouter..get('/{name}', (request) =>
          new Response.ok("Hello ${getPathParameter(request, 'name')}"));

Note without installing the adapter you could still call Shelf Bind's bind method directly.

myRouter.get('/{name}', bind((name) => "Hello ${name}"));

Note: the simplest way to include shelf_bind is to simply use shelf_rest instead of shelf_route

Custom Routeable Adapters

Similarly to how a HandlerAdapter allows you to seamlessly integrate packages that provide alternative forms of Handlers like Shelf Bind, a RouteableAdapter allows you to seamlessly integrate packages that support alternative representations of a RouteableFunction.

RouteableFunction and RouteableAdapter are defined as follows

typedef RouteableFunction(Router router);

typedef RouteableFunction RouteableAdapter(Function handler);

Installation

You can install the adapter when you create the top level router

var myRouteableAdapter = // create some how
var myRouter = router(routeableAdapter: myRouteableAdapter)

Now you can do

myRouter.addAll(new MyResource(), path: 'mine');

and myRouteableAdapter will be called to adapt the instance of MyResource into a RouteableFunction

Note: as with all the adapters you can install them at any level of the routing tree and they will take effect from that point on. For example

myRouter.addAll(new MyResource(), path: 'mine', routeableAdapter: myRouteableAdapter);

Extending (Advanced Usage)

If you can't achieve the customisations you need using the above techniques then you have come to the right place. But first helps to know a little about the architecture of shelf_route.

Architecture

shelf_route is broken into two main parts:

  1. The core routing components such as [Route], [SimpleRoute] and [RequestRouter]. These are immutable components that perform the actual routing of requests.
  2. The router builder components such as [SpecBasedRouterBuilder] and [DefaultRouterBuilder]. These are responsible for building the runtime components (Route etc) and is what normal users interact with when using shelf_route.

Router Builders

Corresponding to the runtime routing components, are pairs of more abstract models. These pairs are an abstract representation of a route, called a route spec, and an adapter that is responsible for creating the corresponding route component from the given route spec. More specifically there are:

  • SimpleRouteSpec and SimpleRouteAdapter which produce SimpleRoute
  • RouterSpec and RouterAdapter which produce RequestRouter
  • RouteSpec and RouteAdapter which are the root of the hierarchy corresponding to Route

Note these models a deliberately very abstract to support the most flexibility possible. However, in almost all circumstances you are more likely to deal with subclasses like DefaultRouterAdapter that provide more concrete implementations. The support ways to provide Middleware and adapt route paths (e.g. supporting different path styles like ':foo') and handlers (such as that provided by shelf_bind that allows normal Dart functions to be used as shelf handlers)

The most advanced form of extensions to shelf_route typically work at this level but producing specs and adapters, either or both of which may be custom subclasses.

Note the Adapters inherit properties from parent routes. So often it is not necessary to provide an adapter at each node in the routing tree. A single one at the top of the tree may be enough.

SpecBasedRouterBuilder, which is also a router spec, has methods to add these specs to builder, such as addRoute

For now the best place to look to understand how to extend shelf_route is the source code of shelf_rest.

More Information

See the wiki for more details on all the options

Contributing

Contributions are welcome. Please:

  1. fork the repo and implement your changes with good unit test coverage of your changes
  2. create a pull request and include enough detail in the description

Issues

See open issues.

Authors

0.14.3

  • handle null values for path parameters

0.14.2

  • updated some dependency versions

0.14.1

  • bug fix: don't use decoded path parameters when creating new Request objects

0.14.0

  • query parameters are now optional

0.13.0

  • the internals have been completely rewritten to improve modularity, flexibility, extensibility, testability and a handful of other ilities. Mostly, this should be transparent to users, except for the noted breaking changes below.
  • removed attach method from Router
  • Dependency versions updated:
    • matcher to <0.12.0
    • uri to <0.10.0
  • Deprecated unittest module replaced with test

0.11.0+1

  • Added complete hello world example to README

0.11.0

  • Added RouteableAdapter to make it cleaner to use packages like shelf_rest

0.10.1

  • split into API and implementation
  • exposed a new library (extend.dart) for those wishing to subclass Router

0.10.0

  • added support for mergeable handler adapters. This allows things like customObjects to be extended by child routes

Note: this is only backward incompatible if you rely on handlerAdapters not being merged. i.e. to override instead

0.9.0

  • Support for Middleware directly on Router (i.e. can now add at top level)
  • Unified Router and _Route via a composite pattern

Note: should be backward compatible with 0.8.x unless you depended on deprecated methods that have been removed

0.8.0+1

  • Don't consume path when exact match is false

0.8.0

  • Changed Router.addAll to take a typedef rather than a class
    • This is backwards incompatible only for anyone that has a class that implements Routeable. To fix this simply use extends Routeable instead.

This change makes it easier to set up hierarchical routes. e.g.

var myRouter = router()
      ..addAll((Router r) => r
        ..get('/foo', (_) =>  new Response.ok('yeah foo'))
        ..get('/fum', (_) =>  new Response.ok('boo fum'))
        ..addAll((Router r) => r
            ..get('/yup/{title}', 
                (req) => new Response.ok("Hello ${getPathParameter(req, 'name')}"))
            ..get('/nup', 
                (req) => new Response.ok("Hello ${getPathParameter(req, 'name')}")),
          path: '/blah/{name}'),
        path: '/bar'
    );

0.7.1

  • Added fallbackHandler to router function
  • call handlerAdapter on fallbackHandler
  • added ALL_METHODS constant

0.7.0+1

  • Fixed use of Deprecated

0.7.0

  • Fixed spelling of Adapter in several classes (thanks Michael Goodness for the contribution)

0.6.0

Major release with lots of changes including:

  • custom path adapters
    • Allows you to plugin any path style you like (e.g. /:foo)
  • custom handler adapters
    • Seamless integration with other Shelf components like Shelf Bind so you can use ordinary Dart functions as Shelf Handlers. For example:
  var myRouter = router(handlerAdapter: bind.handlerAdapter)
  myRouter.get('/{name}', (name) => "Hello ${name}");
  • Support for Shelf Path to remove direct dependency of handlers to Shelf Route
  • Easy to add per route middleware
  • print all routes in a router
  • control over exact path matching
  • Fixed path handling on windows

0.5.0

  • Upgraded to shelf 0.5.1. Note this is only a breaking change for those that directly accessed the path variables in the header. Unfortunately the example and doco did this so I made this a major release

0.4.0

  • Upgraded to shelf 0.5.0

0.3.1

0.3.0

0.2.0+1

  • Fixed handling of routes that are just a path variable

0.2.0

  • Upgraded to shelf 0.4.0

0.1.2+1

  • Fixed bug where body was missing on copied request

0.1.2

  • Added helper function getPathVariables to encapsulate extracting path variables stored in the request

0.1.1

  • Added support for path variables.

  • Improved tests.

1. Depend on it

Add this to your package's pubspec.yaml file:

dependencies:
  shelf_route: "^0.14.3"

2. Install it

You can install packages from the command line:

$ pub get

Alternatively, your editor might support 'pub get'. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:

import 'package:shelf_route/shelf_route.dart';

About

Routing middleware for Shelf

Author

Email andersmholmgren@gmail.com Anders Holmgren

Homepage

bitbucket.org/andersmholmgren/shelf_route

Documentation

www.dartdocs.org/documentation/shelf_route/0.14.3/

Source code (hyperlinked)

www.crossdart.info/p/shelf_route/0.14.3/

Uploader

andersmholmgren@gmail.com

Published

Oct 3, 2016

Share