Routers

Learn about routers and how to implement them

Defining a Router

Routers are the main entry point to a Blueprint application. The router consists of paths (i.e., relative urls) that clients uses to make request against the application. The paths are then bound to controller actions to create routes. Routers are essential application entities. All routers are defined in app/routers.

You define a router by extending the Router class with a router specification, and exporting the extended class from its router module. Below is an example router named message with an empty specification.

app/routers/message.js
const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    // TODO router specification goes here
  }
});

Router Paths

The paths of a router are the relative urls that provide an entry point to the application. If you think of an application as a building, then the paths represent the entryways for the building. Each entryway is in a different location and provides access to a different part of the building.

Paths are defined as keys on the specification property of the router, and begin with a forward slash (/). The example below updates our message router with the single path /messages.

app/routers/message.js
const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
    
    }
  }
});

Now, we have an entryway into our application. We, however, do not know what action we need to perform when a clients wants to use this path.

Reactions to Paths

If you are familiar with HTTP requests, each path in the request requires a HTTP verb, such as GET, POST, PUT, and DELETE. The verb notifies the server (i.e., the application in our case) of what action to execute when a client sends a request to the corresponding path. In our current specification, we have defined a path, but we have not defined what HTTP verb on the path is active, and what action the HTTP verb executes. We call this a reaction.

A reaction is when you define the HTTP verb, and the action its causes.

Let's update our message router to support creating messages.

app/routers/message.js
const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      post: { action: 'message@create' }
    }
  }
});

Now, our message router has defined its first route POST /messages. When a client sends this HTTP request to the application, it will execute the create action on the message controller. We will visit how to implement this action later in the guide. For now, let's focus on the route definition in the message router.

As shown in the message router, the key for nested objects of a path is a HTTP verb. In this example, the HTTP verb is post.

A Blueprint router supports the HTTP verbs defined in the jshttp module.

Actions

The value (or reaction) of the HTTP verb in the router definition can be a controller action. This is signified by the action property in the hash associated with the corresponding HTTP verb. In our example above, the controller action is message@create. This means that POST /messages is going to invoke the create action on the message controller.

There is no one-to-one mapping of paths to controller actions. For example, it is possible to have different paths from the same router, or a different router, bind to the same controller action. This reason for doing so is because either path may have different policies for invoking the action.

Binding to default actions

Some controller may have a single default action named __invoke(). When binding a path to the default action of a controller, you do not need to provide the action name. Instead, just specify the controller name in the action property. The following example illustrates binding the path to the default action for the message controller.

app/routers/message.js
const 
{ Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      post: { action: 'message' }    // bind to default action in message controller
    }
  }
});

Static Views

A view is a document that captures a reusable representation of a response to a request that can be rendered on demand. An example of a view is an HTML document. Similar to actions, you can specify that a path is bound to a static view. Just use the view property instead of the action property for the corresponding route.

app/routers/message.js
const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      get: { view: 'messages' }
    }
  }
});

Now, the GET /messages route will use the messages view to display the messages to the users.

Dynamic Routes

Up until this point, we have been defining static routes. A static route is a route that has a path with no variable parts. A dynamic route therefore is a route that has variable parts. For example, /messages is a static route. But, /messages/1 and messages/2 are dynamic routes. This is because the part of the path after /messages is can change depending what context the client is hoping to access.

We define dynamic routes by including a parameter in the path. A parameter begins with a colon (:). For example, :messageId is a parameter.

Let's define a route for getting a single message:

const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      get: { view: 'message' }
      
      '/:messageId': {
         get { action: 'messages@getOne'}
      }   
    }
  }
});

In the example above, /messages/:messageId is dynamic route. Likewise, the :messageId parameter will be accessible on req.params as req.params.messageId.

Nested Routes

You may have noticed that when we defined the dynamic route for getting a single message, we created a route under /messages. This is call a nested routed. Nested routes is Blueprint's method for allowing you to extend an existing route with a child route, and reduce problems related to defining related routes. The dynamic route from above is the same as this one.

const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      get: { view: 'message' }
    },
    '/messages/:messageId': {
      get { action: 'messages@getOne'}
    }   
  }
});

The main difference between the definition above, and the following one:

const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      get: { view: 'message' }
      
      '/:messageId': {
         get { action: 'messages@getOne'}
      }   
    }
  }
});

is we are inheriting the /messages definition. This means that allow properties, such as policies and Express middleware, of the /messages route will also apply to the /messages/:messageId route.

Using Directories

As your Blueprint application grows, you will find that defining all your routes in a single router will not scale to your needs. This will even be the case with nested routes in single router. To assist with this problem, Blueprint allows you to use directories to define nested routes.

For example, let's assume you are working on v1 of your Blueprint application, and the application has 3 different routers. As part of your design, you want all routes to have a /v1 prefix. The simple approach is to just nest all paths in a router under /v1. This suffices, but it also means you have to do the same for each routers. Likewise, changing the name of the prefix means you have to update each router definition.

An easier, and better, solution would be to not nest all the paths in each router under /v1, but place all routers under the v1/ directory. For example:

app/routers
  - v1/
    - message.js
    - comment.js
    - like.js

Now, all routes in the message, comment, and like router will be prefixed with /v1. For example, /v1/messages and /v1/messages/:messageId are valid routes.

Using directories for nested routes is a easy way to implement versioned routes (e.g., /v1 vs /v2).

Middleware

Express middleware are methods that provide domain-specific functionality to an Express application. For example, you have middleware the logs all the requests to a database, or middleware the authenticates access to a given route. Since Blueprint is a framework built atop Express, it is possible to use Express middleware in a Blueprint application.

Blueprint has built-in middleware for logging, parsing requests, cookies, and validating request input because their load order is important.

You use the use keyword to add middleware to a route. For example, here we are adding the CORS middleware to our route.

const { Router } = require ('@onehilltech/blueprint');
const cors = require ('cors');

module.exports = Router.extend ({
  specification: {
    '/': {
      use: cors ()        // apply CORS middleware to route
    },
    
    '/messages': {
      get: { view: 'message' }
      
      '/:messageId': {
         get { action: 'messages@getOne'}
      }   
    }
  }
});

The use property takes an Express middleware function with the signature function (req, res, next), or an array of Express middleware functions.

Mounting External Routers

One feature you will learn when working with Blueprint modules is you can define routers inside a module for reuse across different applications. The benefit of this feature is the Blueprint module can provide a public access point for how the module can be used by a client. Routers defined in a Blueprint module, however, are not loaded by default. We do this because we want to allow developers to control what routers (and paths) are exposed by the containing application.

This means that developers need a method for defining what routers (and paths) from a Blueprint module are available via the application. We call this process mounting.

To mount a router, you define the path and use the mount() method. Here is an example of mounting a router to the /images path.

const blueprint = require ('@onehilltech/blueprint');

module.exports = {
  '/images': blueprint.mount ('blueprint-images-cdn:images')
}

The example above will mount the images router from the blueprint-images-cdn Blueprint module. If you do not provide a module name (i.e., only use images), then the router is assumed to be part of the application.

Last updated