Skip to content

Instantly share code, notes, and snippets.

@mikermcneil
Last active December 19, 2020 19:30
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mikermcneil/5737561 to your computer and use it in GitHub Desktop.
Save mikermcneil/5737561 to your computer and use it in GitHub Desktop.
Sails.js v0.9 router & blueprints

Router

Sails v0.9: Release Cantidate for 1.0

Important note on plugins

After hearing great points from the community, and experiencing the need for this in some of our projects, all but the bare bones Sails core is going to be pulled into plugins. The basic plugins will all be installed by default in new projects, but now it's possible to disable the things you don't want. It also makes it much easier to customize Sails for your needs, and probably enables all sorts of cool things none of us have even considered yet. ** More on this to come **

  • REST blueprints will now be applied using the controllers hook, and configurable-- on by default.
  • Automatic controller routing (add a middleware method to a controller and you can hit it at /controller/middleware) will also be applied via the controllers hook, on by default, and is now considered part of the blueprints (the other blueprints just happen only to work if you have a model, since they need something to get allllll RESTful on.)
  • csrf is controlled via sails.config.controllers.csrf as part of the controllers hook, false by default.
  • i18n is controlled via sails.config.views.i18n as part of the views hook, true by default.

Philosophy

  • The only thing better than thin controllers is no controllers.
  • Middleware is good- it's nice to do a little HTTP traffic control in a more declarative way, and can dramatically simplify your workflow as you extend the application. historically, we've called them policies. The last middleware is your controller.
  • Architecture must allow for dynamic rebuilding of the router at runtime in development mode, and router should get out of the way in production and pipe requests directly into Express's and Socket.io's routers whenever possible to maximize performance. In general, if you don't ever want a piece of code to be accesible via the server, don't put it in a controller. Instead, create a service or add the logic as a method for one of your models.

Migration Guide

  • Routes can now specify a list of targets, which will be run in order (this allows for binding chains of middleware directly to routes). Both controllers (controller.action) and arbitrary middleare (middleware) functions from the new, optional middleware directory can be specified, in any order:
{
  'post /signup': ['user.unique', 'user.create', 'verifyemail.send'],
  '/auth/logout': ['authenticated', 'auth.logout']
}

You can also still use classic {controller: 'foo', action: 'bar' } notation.

And conveniently, miscellaneous strings are treated as redirects:

{
  // Alias '/logout' to '/auth/logout'
  'get /logout': '/auth/logout',
  '/thegoogle': 'http://google.com'
}
  • Legacy policy config still works-- if config/policies.js is specified, the provided policies will be mapped to controllers/actions, vs. the new routing options which allow multiple actions and policies to be intermingled as arbitrary middleware bound at the route level. Both concepts can ceoexist- but the policies.js config file won't appear in new projects by default.
  • Methods in controllers like ControllerName['get foo']() now automatically routed to get /controllername/foo instead of get /controllername/get%20foo For example, if you have a DogController and you add the method put index, a route will be generated: put /dog. Note that this will override the blueprint's RESTful update() method.

New Configuration Options

sails.config.controller.csrf

  • sails.config.csrf Defaults to false. If truthy, all non-GET requests require a special CSRF id to prove that the request is originating from an acceptable origin (prevents session hijacking attack). Under the covers, the hook uses the Express csrf middleware to set a _csrf available in your server-side views.

    If you're creating a single-page web app, a phonegap app, a chrome extension, or otherwise can't get a hold of the _csrf token since you're never getting a rendered server-side view, the csrf hook automatically binds a custom route just for you. If set to csrf: true, the default route of get /csrfToken is created, which responds with a JSON object: { "_csrf":"blahblah" }. Insteading of setting the config to true, you can also specify a route, in which case the csrf middleware will be served on that route instead. Keep in mind it needs to be a GET requests will work, since the whole point of this is to prevent other types of requests until the user has a valid token.

sails.config.controller.blueprints.routes

  • sails.config.controller.blueprints.routes is an object containing all of the automatically generated blueprint routes, allowing you to cherry pick or override the blueprints you want enabled in your app. By default, the default route patterns are all set to true, which indicates that the defaults should be use. If you set one of the routes to false, it will be disabled globally. If you set it to a custom middleware function, that function will be used instaed. Note that this isn't just for the blueprint REST API-- this is also where the automatic routing takes place that allows you to add a method to a controller and have a route automatically generated. The RESTful routes (create, destroy, find, etc.) require that a corresponding model exist (e.g. in order for RESTful blueprints to do anything with /user, you need both a UserController.js and a User.js model.) This is a global setting that applies to blueprints in all of your controllers.

sails.config.controller.blueprints.explicitIntegerId

  • sails.config.controllers.blueprints.explicitIntegerId is set to true by default. When enabled, only natural numbers will match the :id route parameter. You'd want to disable this if you want to lookup models by mongo id, or a custom, non-integer primary key blueprints. This is a global setting that applies to blueprints in all of your controllers.

sails.config.controller.blueprints.prefix

  • sails.config.controllers.blueprints.prefix is set to '' by default. If you set this key, all blueprint routes will be set up using the route prefix you specify. e.g. if prefix is /api/v2, the blueprint routes for the DeliveryController will be served as get /api/v2/delivery, post /api/v2/delivery, /api/v2/someMethodInYourController, and so on. This is a global setting that applies to blueprints in all of your controllers.

Routing order

Note: The configured policies in config/policies.js are applied in-order for their targeted controller or action wherever that controller or action is routed to. When configured in this way, they are completely separate from routes. This is convenient since you might like to access a controller in more than one way, and want it to be protected no matter what URL or HTTP method you access it with.

  • Custom Express middleware (usually calls next(), and so requests are allowed to continue down the list below)
  • Custom routes in routes.js config
  • Controller methods
  • Automatically serve views that match the requested path structure
  • CRUD blueprints

As always, if you're stuck, feel free to reach out to the Google Group or the #sailsjs IRC on freenode!

-Mike

@dasher
Copy link

dasher commented Jun 12, 2013

Wotcha Mike,

I was thinking of the targets on routes last night & policy chains.
One of the problems we face is with admin routes - essentially you fold in a set of controller actions & policies under a namespace. For routing - this means 2 things:

  1. usually ends up adding a specific set of policy chains for roles, prefetching some data or some such
  2. tweaking a lot of the routes to apply these things.
    To simplify this with the new approach - it would be helpful if we could use nested arrays in the policy-chain on routes.

Nested arrays would just be _.flatten() when the policy is compiled.

This would allow something like:

var superUsersRole = ['policyA', 'policyB', 'controllerA.actionA'];
var someOtherRole = ['policyC', 'policyD', 'controllerA.actionB'];

{
  '/admin/siteStats': ['authenticated', superUsersRole, 'notifications.my'],
  '/admin/dashboard': ['authenticated', someOtherRole],
  '/admin/issues': ['authenticated', someOtherRole, superUserRole, 'someOtherPolicy'],
}

Basically allowing shorthand and a single point to be changed as the code evolves or the app changes.

The 2nd thing is socket routing - we sometimes use versioned namespaced sockets - this helps a lot in testing on the clientside, prototyping apps and mashups.

Clientside this looks like:

  apiv1 = io.connect('http://localhost/api@0.1.0')
  apiv2 = io.connect('http://localhost/api@0.2.0')

and then on the serverside - we can run and remap:

var user = require("controllers/users");
var serverInfo = require("controllers/server");

var apiv1 = io
  .of('/api@0.1.0')
  .on('connection', function (socket) {
    socket.emit('info', {
        'version': socket.endpoint
    });
    socket.on('fetchUsers', user.fetchV1);
    socket.on('stats', serverinfo.fetchV1);
  })

var apiv2 = io
  .of('/api@0.2.0')
  .on('connection', function (socket) {
    socket.emit('info', {
        'version': socket.endpoint
    });
    socket.on('fetchUsers', user.fetchV1);
    socket.on('stats', serverinfo.fetchV2);
  })

Any thoughts - with the work on routing - on how we can simplify sockety type work like this?
I know this is a bit of an edge case - but it's extremely helpful to mash up these things and some spiffy route style approach would be pretty handy.

Often routing acts as a translation layer|buffer between the frontend and the backend - esp in large apps.

@vizo
Copy link

vizo commented Jun 24, 2013

+1

@mikermcneil
Copy link
Author

Re: the 1st one-- definitely, on the roadmap!

Re the 2nd-- I've seen several people bring this up, and I'd like to support it, but I do wonder what it would look like?

Hmm..... your namespace in this case is functioning a lot like /api/v2 would for HTTP APIs. A sensible way to approach the problem might be using namespaces to set the path prefix that will be used subsequently for all "requests" from that socket. This also has the benefit of getting sockets on the same page as HTTP with versioned APIs. For example:

module.exports.routes = {
  'get /api/v2/user': {
    controller: 'user',
    action: 'find'
  }
};

You could either send a GET to /api/v2/user via HTTP, or you could connect a socket on the /api/v2 namespace, then from your client-side code, use socket.request('/user', {}, function (response) {/* ... */} ) to get the same effect.

@polastre
Copy link

polastre commented Aug 8, 2013

Mike, is the following implemented in Sails 0.9.3? It doesn't seem like putting 'get foo' in module.exports of a controller overwrites the restful blueprint.

In what file would you recommend overriding sails.config.controller.blueprints.routes?

And is there any way to override a blueprint route like /foo/:id such that other custom routes (like /foo/test) are still routed to the function in module.exports for the controller?

Methods in controllers like ControllerName['get foo']() now automatically routed to get /controllername/foo instead of get /controllername/get%20foo For example, if you have a DogController and you add the method put index, a route will be generated: put /dog. Note that this will override the blueprint's RESTful update() method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment