Routing

This document contains a simple and fun introduction to the mojo.js router and its underlying concepts.

Concepts

Essentials every mojo.js developer should know.

Dispatcher

The foundation of every web framework is a tiny black box connecting incoming requests with code generating the appropriate response.

  1. GET /user/show/1 -> ctx.render({text: 'Daniel'});

This black box is usually called a dispatcher. There are many implementations using different strategies to establish these connections, but pretty much all are based around mapping the path part of the request URL to some kind of response generator.

  1. /user/show/2 -> ctx.render({text: 'Isabell'});
  2. /user/show/3 -> ctx.render({text: 'Sara'});
  3. /user/show/4 -> ctx.render({text: 'Stefan'});
  4. /user/show/5 -> ctx.render({text: 'Fynn'});

While it is very well possible to make all these connections static, it is also rather inefficient. That’s why regular expressions are commonly used to make the dispatch process more dynamic.

  1. qr!/user/show/(\d+)! -> ctx.render({text: users[match[1]]});

Modern dispatchers have pretty much everything HTTP has to offer at their disposal and can use many more variables than just the request path, such as request method and headers like Host, User-Agent and Accept.

  1. GET /user/show/23 HTTP/1.1
  2. Host: mojolicious.org
  3. User-Agent: Mojolicious (Perl)
  4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Routes

While regular expressions are quite powerful they also tend to be unpleasant to look at and are generally overkill for ordinary path matching.

  1. qr!/user/admin/(\d+)! -> ctx.render({text: users[match[1]]);

This is where routes come into play, they have been designed from the ground up to represent paths with placeholders.

  1. /user/admin/:id -> ctx.render({text: users[id]});

The only difference between a static path and the route above is the :id placeholder. One or more placeholders can be anywhere in the route.

  1. /user/:role/:id

A fundamental concept of the mojo.js router is that extracted placeholder values are turned into an object.

  1. /user/admin/23 -> /user/:role/:id -> {role: 'admin', id: 23}

This object is basically the center of every mojo.js application, you will learn more about this later on. Internally, routes get compiled to regular expressions, so you can get the best of both worlds with a little bit of experience.

  1. /user/admin/:id -> /(?:^\/user\/admin\/([^\/.]+))/

A trailing slash in the path is always optional.

  1. /user/admin/23/ -> /user/:role/:id -> {role: 'admin', id: 23}

Reversibility

One more huge advantage routes have over regular expressions is that they are easily reversible, extracted placeholders can be turned back into a path at any time.

  1. /sebastian -> /:name -> {name: 'sebastian'}
  2. {name: 'sebastian'} -> /:name -> /sebastian

Every placeholder has a name, even if it’s just an empty string.

Standard Placeholders

Standard placeholders are the simplest form of placeholders, they use a colon prefix and match all characters except / and ., similar to the regular expression ([^/.]+).

  1. /hello -> /:name/hello -> null
  2. /sebastian/23/hello -> /:name/hello -> null
  3. /sebastian.23/hello -> /:name/hello -> null
  4. /sebastian/hello -> /:name/hello -> {name: 'sebastian'}
  5. /sebastian23/hello -> /:name/hello -> {name: 'sebastian23'}
  6. /sebastian 23/hello -> /:name/hello -> {name: 'sebastian 23'}

All placeholders can be surrounded by < and > to separate them from the surrounding text.

  1. /hello -> /<:name>hello -> null
  2. /sebastian/23hello -> /<:name>hello -> null
  3. /sebastian.23hello -> /<:name>hello -> null
  4. /sebastianhello -> /<:name>hello -> {name: 'sebastian'}
  5. /sebastian23hello -> /<:name>hello -> {name: 'sebastian23'}
  6. /sebastian 23hello -> /<:name>hello -> {name: 'sebastian 23'}

The colon prefix is optional for standard placeholders that are surrounded by < and >.

  1. /i♥mojolicious -> /<one>♥<two> -> {one: 'i', two: 'mojolicious'}

Relaxed Placeholders

Relaxed placeholders are just like standard placeholders, but use a hash prefix and match all characters except /, similar to the regular expression ([^/]+).

  1. /hello -> /#name/hello -> null
  2. /sebastian/23/hello -> /#name/hello -> null
  3. /sebastian.23/hello -> /#name/hello -> {name: 'sebastian.23'}
  4. /sebastian/hello -> /#name/hello -> {name: 'sebastian'}
  5. /sebastian23/hello -> /#name/hello -> {name: 'sebastian23'}
  6. /sebastian 23/hello -> /#name/hello -> {name: 'sebastian 23'}

They can be especially useful for manually matching file names with extensions.

  1. /music/song.mp3 -> /music/#filename -> {filename: 'song.mp3'}

Wildcard Placeholders

Wildcard placeholders are just like the two types of placeholders above, but use an asterisk prefix and match absolutely everything, including / and ., similar to the regular expression (.+).

  1. /hello -> /*name/hello -> null
  2. /sebastian/23/hello -> /*name/hello -> {name: 'sebastian/23'}
  3. /sebastian.23/hello -> /*name/hello -> {name: 'sebastian.23'}
  4. /sebastian/hello -> /*name/hello -> {name: 'sebastian'}
  5. /sebastian23/hello -> /*name/hello -> {name: 'sebastian23'}
  6. /sebastian 23/hello -> /*name/hello -> {name: 'sebastian 23'}

They can be useful for manually matching entire file paths.

  1. /music/rock/song.mp3 -> /music/*filepath -> {filepath: 'rock/song.mp3'}

Basics

Most commonly used features every mojo.js developer should know about.

Minimal Route

The router property of every mojo.js application contains a router object you can use to generate route structures.

  1. import mojo from '@mojojs/core';
  2. // Application
  3. const app = mojo();
  4. // Router
  5. const router = app.router;
  6. // Route
  7. router.get('/welcome').to({controller: 'foo', action: 'welcome'});
  8. app.start();

The minimal route above will load and instantiate the controller controllers/foo.js and call its welcome method. Routes are usually configured in the main application script (often called index.js), but the router can be accessed from everywhere (even at runtime).

  1. // Controller
  2. export default class FooController {
  3. // Action
  4. async welcome(ctx) {
  5. // Render response
  6. await ctx.render({text: 'Hello there.'});
  7. }
  8. }

All routes match in the same order in which they were defined, and matching stops as soon as a suitable route has been found. So you can improve the routing performance by declaring your most frequently accessed routes first. A routing cache will also be used automatically to handle sudden traffic spikes more gracefully.

Routing Destination

After you start a new route with methods like get, you can also give it a destination in the form of an object using the chained method to.

  1. // GET /welcome -> {controller: 'foo', action: 'welcome'}
  2. router.get('/welcome').to({controller: 'foo', action: 'welcome'});

Now if the route matches an incoming request it will use the content of this object to try and find appropriate code to generate a response.

HTTP Methods

There are already shortcuts for the most common HTTP request methods like post, and for more control any accepts an optional array with arbitrary request methods as first argument.

  1. // PUT /hello -> null
  2. // GET /hello -> {controller: 'foo', action: 'hello'}
  3. router.get('/hello').to({controller: 'foo', action: 'hello'});
  4. // PUT /hello -> {controller: 'foo', action: 'hello'}
  5. router.put('/hello').to({controller: 'foo', action: 'hello'});
  6. // POST /hello -> {controller: 'foo', action: 'hello'}
  7. router.post('/hello').to({controller: 'foo', action: 'hello'});
  8. // GET|POST /bye -> {controller: 'foo', action: 'bye'}
  9. router.any(['GET', 'POST'], '/bye').to({controller: 'foo', action: 'bye'});
  10. // * /whatever -> {controller: 'foo', action: 'whatever'}
  11. router.any('/whatever').to({controller: 'foo', action: 'whatever'});

There is one small exception, HEAD requests are considered equal to GET, but content will not be sent with the response even if it is present.

  1. // GET /test -> {controller: 'bar', action: 'test'}
  2. // HEAD /test -> {controller: 'bar', action: 'test'}
  3. router.get('/test').to({controller: 'bar', action: 'test'});

IRIs

IRIs are handled transparently, that means paths are guaranteed to be unescaped and decoded from bytes to characters.

  1. // GET /☃ (Unicode snowman) -> {controller: 'foo', action: 'snowman'}
  2. router.get('/☃').to({controller: 'foo', action: 'snowman'});

Stash

The generated object of a matching route is actually the center of the whole mojo.js request cycle. We call it the stash, and it persists until a response has been generated.

  1. // GET /bye -> {controller: 'foo', action: 'bye', mymessage: 'Bye'}
  2. router.get('/bye').to({controller: 'foo', action: 'bye', mymessage: 'Bye'});

There are a few stash values with special meaning, such as controller and action, but you can generally fill it with whatever data you need to generate a response. Once dispatched the whole stash content can be changed at any time.

  1. // Action
  2. async bye(ctx) {
  3. // Get message from stash
  4. const msg = ctx.stash.mymessage;
  5. // Change message in stash
  6. ctx.stash.mymessage = 'Welcome';
  7. // Render a template that might use stash values
  8. await ctx.render({view: 'bye'});
  9. }

Nested Routes

It is also possible to build tree structures from routes to remove repetitive code. A route with children can’t match on its own though, only the actual endpoints of these nested routes can.

  1. // GET /foo -> null
  2. // GET /foo/bar -> {controller: 'foo', action: 'bar'}
  3. const foo = router.any('/foo').to({controller: 'foo'});
  4. foo.get('/bar').to({action: 'bar'});

The stash is simply inherited from route to route and newer values override old ones.

  1. // GET /cats -> {controller: 'cats', action: 'index'}
  2. // GET /cats/nyan -> {controller: 'cats', action: 'nyan'}
  3. // GET /cats/lol -> {controller: 'cats', action: 'default'}
  4. const cats = router.any('/cats').to({controller: 'cats', action: 'default'});
  5. cats.get('/').to({action: 'index'});
  6. cats.get('/nyan').to({action: 'nyan'});
  7. cats.get('/lol');

With a few common prefixes you can also greatly improve the routing performance of applications with many routes, because children are only tried if the prefix matched first.

Special Stash Values

When the dispatcher sees controller and action values in the stash it will always try find a controller instance and method to dispatch to. By default, the router will load and instantiate all controller classes from the controllers directory during application startup.

  1. // Application ("index.js")
  2. import mojo from '@mojojs/core';
  3. const app = mojo();
  4. const router = app.router;
  5. // GET /bye -> "controllers/foo.js"
  6. router.get('/bye').to({controller: 'foo', action: 'bye'});
  7. app.start();
  1. // Controller ("controllers/foo.js")
  2. export default class FooController {
  3. // Action
  4. async bye(ctx) {
  5. // Render response
  6. await ctx.render({text: 'Good bye.'});
  7. }
  8. }

Controller classes are perfect for organizing code in larger projects. There are more dispatch strategies, but because controllers are the most commonly used ones they also got a special shortcut in the form of controller#action.

  1. // GET /bye -> {controller: 'foo', action: 'bye'}
  2. router.get('/bye').to('foo#bye');

Route to Function

The fn stash value, which won’t be inherited by nested routes, can be used to bypass controllers and execute a function instead.

  1. router.get('/bye').to({fn: async ctx => {
  2. await ctx.render({text => 'Good bye.'});
  3. });

But you can also just pass the callback directly to get (and similar methods), which usually looks much better.

  1. router.get('/bye', async ctx => {
  2. await ctx.render({text => 'Good bye.'});
  3. });

Named Routes

Naming your routes will allow backreferencing in many methods and helpers throughout the whole framework, most of which internally rely on ctx.urlFor() for this.

  1. // GET /foo/marcus -> {controller: 'foo', action: 'bar', user: 'marcus'}
  2. router.get('/foo/:user').to('foo#bar').name('baz');
  1. // Generate URL "/foo/marcus" for route "baz" (in previous request context)
  2. const url = ctx.urlFor('baz');
  3. // Generate URL "/foo/jan" for route "baz"
  4. const url = ctx.urlFor('baz', {values: {user: 'jan'}});

You can manually assign a name or let the router generate one automatically, which would be equal to the route itself with all non-word characters replaced with the _ character. Custom names have a higher precedence.

  1. // GET /foo/bar ("foobar")
  2. router.get('/foo/bar').to('test#stuff');
  1. // Generate URL "/foo/bar"
  2. const url = ctx.urlFor('foo_bar');

To refer to the current route you can use the reserved name current or no name at all.

  1. // Generate URL for current route
  2. const url = ctx.urlFor('current');
  3. const url = ctx.urlFor();

To check or get the name of the current route you can use the helper ctx.currentRoute().

  1. // Name for current route
  2. const name = ctx.currentRoute();
  3. // Check route name in code shared by multiple routes
  4. if (if ctx.currentRoute() === 'login') ctx.stash.button = 'green';

Optional Placeholders

Extracted placeholder values will simply redefine older stash values if they already exist.

  1. // GET /bye -> {controller: 'foo', action: 'bar', mymessage: 'bye'}
  2. // GET /hey -> {controller: 'foo', action: 'bar', mymessage: 'hey'}
  3. router.get('/:mymessage').to({controller: 'foo', action: 'bar', mymessage: 'hi'});

One more interesting effect, a placeholder automatically becomes optional if there is already a stash value of the same name present, this works similar to the regular expression ([^/.]+)?.

  1. // GET / -> {controller: 'foo', action: 'bar', mymessage: 'hi'}
  2. router.get('/:mymessage').to({controller: 'foo', action: 'bar', mymessage: 'hi'});
  3. // GET /test/123 -> {controller: 'foo', action: 'bar', mymessage: 'hi'}
  4. // GET /test/bye/123 -> {controller: 'foo', action: 'bar', mymessage: 'bye'}
  5. router.get('/test/:mymessage/123').to({controller: 'foo', action: 'bar', mymessage: 'hi'});

And if two optional placeholders are only separated by a slash, that slash can become optional as well.

Restrictive Placeholders

A very easy way to make placeholders more restrictive are alternatives, you just make a list of possible values, which then work similar to the regular expression (bender|leela).

  1. // GET /fry -> null
  2. // GET /bender -> {controller: 'foo', action: 'bar', name: 'bender'}
  3. //GET /leela -> {controller: 'foo', action: 'bar', name: 'leela'}
  4. router.get('/:name', {'name': ['bender', 'leela']}).to('foo#bar');

You can also adjust the regular expressions behind placeholders directly, just make sure not to use ^ and $ or capturing groups (...), because placeholders become part of a larger regular expression internally, non-capturing groups (?:...) are fine though.

  1. // GET /23 -> {controller: 'foo', action: 'bar', number: 23}
  2. // GET /test -> null
  3. router.get('/:number', {number: /\d+/}).to('foo#bar');
  4. // GET /23 -> null
  5. // GET /test -> {controller: 'foo', action: 'bar', name: 'test'}
  6. router.get('/:name', {name: /[a-zA-Z]+/}).to('foo#bar');

This way you get easily readable routes and the raw power of regular expressions.

Placeholder Types

And if you have multiple routes using restrictive placeholders you can also turn them into placeholder types with app.router.addType.

  1. // A type with alternatives
  2. router.addType('futurama_name', ['bender', 'leela']);
  3. // GET /fry -> null
  4. // GET /bender -> {controller: 'foo', action: 'bar', name: 'bender'}
  5. // GET /leela -> {controller: 'foo', action: 'bar', name: 'leela'}
  6. router.get('/<name:futurama_name>').to('foo#bar');

Placeholder types work just like restrictive placeholders, they are just reusable with the <placeholder:type> notation.

  1. // A type adjusting the regular expression
  2. router.addType('upper', /[A-Z]+/);
  3. // GET /user/ROOT -> {controller: 'users', action: 'show', name: 'ROOT'}
  4. // GET /user/root -> null
  5. // GET /user/23 -> null
  6. router.get('/user/<name:upper>').to('users#show');

Some types like num are used so commonly that they are available by default.

  1. // GET /article/12 -> {controller: 'article', action: 'show', id: 12}
  2. // GET /article/test -> null
  3. router.get('/article/<id:num>').to('articles#show');

Introspection

The routes command can be used from the command line to list all available routes together with names and underlying regular expressions.

  1. $ node myapp.js routes -v
  2. /foo/:name POST fooname /^\/foo\/([^/.]+)/s
  3. /bar * bar /^\/bar/s
  4. +/baz GET baz /^\/baz\/([^/.]+)/s
  5. /yada * yada /^\/yada\/([^/.]+)/s

Under

To share code with multiple nested routes you can use app.router.under, because unlike normal nested routes, the routes generated with it have their own intermediate destination and result in additional dispatch cycles when they match.

  1. // GET /foo -> null
  2. // GET /foo/bar -> {controller: 'foo', action: 'baz'}
  3. // {controller: 'foo', action: 'bar'}
  4. const foo = router.under('/foo').to('foo#baz');
  5. foo.get('/bar').to('#bar');

The actual action code for this destination can return false to break the dispatch chain, this can be a very powerful tool for authentication.

  1. // GET /blackjack -> {fn: ctx => {...}}
  2. // {controller: 'hideout', action: 'blackjack'}
  3. const auth = router.under('/' => async ctx => {
  4. // Authenticated
  5. if (ctx.req.headers['x-bender'] !== undefined) return;
  6. // Not authenticated
  7. await ctx.render({text: "You're not Bender.", status: 401});
  8. return false;
  9. });
  10. auth.get('/blackjack').to('hideout#blackjack');

Every destination is just a snapshot of the stash at the time the route matched. For a little more power you can introspect the preceding and succeeding destinations with ctx.plan.

  1. // Action of the fourth dispatch cycle
  2. const action = ctx.plan.step[3].action;

Extensions

File extensions like .html and .txt at the end of a route can be detected and stored in the stash value ext. Use a restrictive placeholder to declare the possible values.

  1. // GET /foo.txt -> null
  2. // GET /foo.rss -> {controller: 'foo', action: 'bar', format: 'rss'}
  3. // GET /foo.xml -> {controller: 'foo', action: 'bar', format: 'xml'}
  4. router.get('/foo', {ext: ['rss', 'xml']}).to('foo#bar');

And just like with placeholders you can use a default value to make the extension optional.

  1. // GET /foo -> {controller: 'foo', action: 'bar'}
  2. // GET /foo.html -> {controller: 'foo', action: 'bar', ext: 'html'}
  3. // GET /foo.txt -> {controller: 'foo', action: 'bar', ext: 'txt'}
  4. router.get('/foo', {ext: ['html', 'txt']}).to({controller: 'foo', action: 'bar', ext: null);

An extension value can also be passed to ctx.urlFor().

  1. // GET /foo/23.txt -> {controller: 'foo', action: 'bar', id: 23, ext: 'txt'}
  2. router.get('/foo/:id', {ext: ['txt', 'json']}).to('foo#bar').name('baz');
  1. // Generate URL "/foo/24.json" for route "baz"
  2. const url = ctx.urlFor('baz', {values: {id: 24, ext: 'txt'}});

WebSockets

With the websocket method of the router you can restrict access to WebSocket handshakes, which are normal GET requests with some additional information.

  1. // WebSocket handshake route ("index.js")
  2. app.websocket('/echo').to('foo#echo');
  1. // Controller ("controllers/foo.js")
  2. export default class FooController {
  3. // Action
  4. echo(ctx) {
  5. ctx.plain(async ws => {
  6. for await (const message of ws) {
  7. await ws.send(`echo: ${message}`);
  8. }
  9. });
  10. }
  11. }

The context methods plain and json can be used to accept the incoming WebSocket connection either in plain text/binary message mode, or with automatic JSON encoding and decoding.

  1. export default class BarController {
  2. addFuturamaQuote(ctx) {
  3. // Activate JSON mode
  4. ctx.json(async ws => {
  5. for await (const data of ws) {
  6. // Add a Futurama quote to JSON objects
  7. if (typeof data === 'object') {
  8. data.quote = 'Shut up and take my money!';
  9. await ws.send(data);
  10. }
  11. // Close the connection for everything else
  12. else {
  13. ws.close();
  14. }
  15. }
  16. });
  17. }
  18. }

The close method is used to end an established WebSocket connection. To reject an incoming connection completely, just don’t do anything at all, the rejection will happen automatically.

  1. GET /echo HTTP/1.1
  2. Host: mojolicious.org
  3. User-Agent: Mojolicious (Perl)
  4. Connection: Upgrade
  5. Upgrade: websocket
  6. Sec-WebSocket-Key: IDM3ODE4NDk2MjA1OTcxOQ==
  7. Sec-WebSocket-Version: 13
  8. HTTP/1.1 101 Switching Protocols
  9. Server: Mojolicious (Perl)
  10. Date: Tue, 03 Feb 2015 17:08:24 GMT
  11. Connection: Upgrade
  12. Upgrade: websocket
  13. Sec-WebSocket-Accept: SWsp5N2iNxPbHlcOTIw8ERvyVPY=

On the protocol level, the connection gets established with a 101 HTTP response. The handshake request can contain any number of arbitrary HTTP headers, this can be very useful for authentication.

Catch-all Route

Since routes match in the order in which they were defined, you can catch all requests that did not match in your last route with an optional wildcard placeholder.

  1. // * /*
  2. router.any('/*whatever').to({whatever: '', fn: async ctx => {
  3. const params = await ctx.params();
  4. const whatever = params.whatever;
  5. await ctx.render({text: `/${whatever} did not match.`, status: 404});
  6. }});

Conditions

Conditions such as headers and host can be applied to any route with route.requires, and allow even more powerful route constructs.

  1. // GET / (Origin: http://perl.org)
  2. router.get('/').requires('headers', {Origin: /perl\.org/}}).to('foo#bar');
  3. // GET http://docs.mojolicious.org/Mojolicious
  4. router.get('/').requires('host', /docs\.mojolicious\.org/).to('perldoc#index');

Just be aware that conditions are too complex for the routing cache, which normally speeds up recurring requests, and can therefore reduce performance.

Hooks

Hooks operate outside the routing system and allow you to extend the framework itself by running code at different phases in the lifecycle of your application and/or every request. That makes them a very powerful tool especially for plugins.

  1. import mojo from '@mojojs/core';
  2. const app = mojo();
  3. const router = app.router;
  4. // Check all requests for a "/test" path
  5. app.addContextHook('dispatch:before', async ctx => {
  6. if (ctx.req.path !== '/test') return;
  7. await ctx.render({text: 'This request did not reach the router.'});
  8. return true;
  9. });
  10. // These will not be reached if the hook above renders a response
  11. router.get('/welcome').to('foo#welcome');
  12. router.get('/bye').to('foo#bye');
  13. app.start();

Every handler can return a value other than undefined to prevent followup handlers positioned later in the hook chain from running. Additionally, some hooks also use specific return values like true to trigger effects such as intercepting followup logic.

See the Cheatsheet for a full list of hooks that are currently available by default.

Advanced

Less commonly used and more powerful features.

Rearranging Routes

From application startup until the first request has arrived, all routes can still be moved around or even removed with methods like route.addChild() and route.remove().

  1. // GET /show -> null
  2. // GET /example/show -> {controller: 'example', action: 'show'}
  3. const show = router.get('/show').to('example#show');
  4. router.any('/example').addChild(show);
  5. // Nothing
  6. router.get('/secrets/show').to('secrets#show').name('show_secrets');
  7. router.find('show_secrets').remove();

Especially for rearranging routes created by plugins this can be very useful, to find routes by their name you can use router.find().

Adding Conditions

You can also add your own conditions with router.addCondition(). All conditions are basically router plugins that run every time a new request arrives, and which need to return true for the route to match.

  1. // A condition that randomly allows a route to match
  2. router.addCondition('random', async (ctx, num) => {
  3. // Winner
  4. if (Math.floor(Math.random() * 10) === num) return true;
  5. // Loser
  6. return false;
  7. });
  8. // GET /maybe (10% chance)
  9. router.get('/maybe').requires('random', 5).to('foo#bar');

As async functions, conditions can access a wide range of information.

  1. // A condition to check query parameters (useful for mock web services)
  2. router.addCondition('query', async (ctx, values) => {
  3. const params = await ctx.params();
  4. for (const [name, value] of Object.entries(values)) {
  5. if (params.get(name) !== value) return false;
  6. }
  7. return true;
  8. });
  9. // GET /hello?to=world&test=1
  10. router.get('/hello').requires('query', {test: '1', to: 'world'}).to('foo#bar');

Support

If you have any questions the documentation might not yet answer, don’t hesitate to ask in the Forum, on Matrix, or IRC.