Routers & Routes

We have implemented a controller with a single action that returns a list of rentals. The controller and its actions by itself cannot handle requests. It is not until we bind a controller's action with a route are we able to handle requests from client.

The route serves as the public facing access point to the Blueprint application. The route is consists of an HTTP verb (e.g., GET, DELETE, POST, and PUT) and a path (e.g., /a/b/c). In the super-rentals example, there is the single route GET /api/rentals. Because we want our Blueprint application to serve as the API service for the super-rentals example, we need our application to define the same route as expect by the EmberJS application.

Defining Your Router

All routes are defined in a router. For our work, we are are going to define our route in the router named rental. There are several approaches we can use when defining a router, which depends on how much reuse we want across a routes paths. The first approach is we can define all routes in a single router with nested definitions. For example:

module.exports = {
  '/a': {
    '/b': {
      // add actions here
    }
  }
}

This approach works well if you are not modularizing your router definition such that a single router focuses on a single aspect of the application. Instead, you have a single monolithic router that defines every route in your application.

As you scale your application to contain routes for many facets of the application, you will find this approach becomes hard to maintain. Moreover, it will be hard to mount a router defined inside a Blueprint module since the mounted router may define more routes than you want to expose publicly from your Blueprint application.

The second approach is to modularize your routes across different routers. This approach, however, makes it hard to reuse parts of the routes path across different router definitions. To address this problem, you can place different routers that share common base paths in the same subdirectory structure. The names of the subdirectories will constitute the base paths for the routes defined in each router.

Since we want to plan for growth, and showcase how routers in subdirectories work, we will opt for the second approach of defining routers inside of subdirectories.

As mentioned before, the super-rentals example has a single route GET /api/rentals. Let's assume that if we want to define other routes, they will have the base path /api. We therefore want to define our routers in the subdirectory named api. Let's start with the single router named rental.

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

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

As you will notice about, we define a router with the single path /rentals. We do not include /api in the path definition because this router is located in the api subdirectory. This means that routes defined in routers located in the api subdirectory will be prefixed with /api. If we placed this same router in the subdirectory named v1, which is a subdirectory of api, then the base path will be /api/v1.

Defining routers in subdirectories is the recommended approach to versioning routes in your Blueprint application.

Binding Your Route to an Action

We have defined the route for the application, but we need the route to perform an action when the client makes a request to /api/rentals. As previously discussed, the HTTP verb we need to respond to is the GET verb. We already have implemented the action that returns a list of rentals. Let's bind this path to that specific action.

Update the /rental path in the router specification with the code below.

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

module.exports = Router.extend ({
  specification: {
    '/rentals': {
      // This statement will bind this route to the get action in the 
      // rental controller. Now, GET /api/rentals can handle client requests.
      get: {action: 'rental@get'}
    }
  }
});

Now, you should be able to go to the url http://localhost:5000/api/rentals in your favorite browser, and it will display the list of rentals we defined in the rental.get action.

Integrating with EmberJS

Since we are basing this tutorial on the super-rental tutorial in EmberJS, it is only fitting that we show you how to integrate the Blueprint application with the EmberJS super-rental application. There is minimal work needed to replace mirage with this Blueprint application. The main approach is to leverage the power of adapters and serializers in ember-data, which allows an EmberJS application to integrate with virtually any backend api service, including Blueprint.

Getting started, we made our life easy by returning data from our route in the JSON-API specification because the super-rental application has configured ember-data to handle JSON-API by default. We just have to instruct the application to retrieve the data from the Blueprint application instead of from mirage. We do this by implementing an application adapter that defines where the data is located.

app/adapters/application.js
import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend ({
  host: 'http://localhost:5000',
  namespace: 'api'
});

Voila!

There is nothing more that you need to write.

You may have to disable mirage in your EmberJS application configuration if you do not see the Blueprint application handling requests from the EmberJS application.

Now, when you use the EmberJS application, you should notice the Blueprint application handling request from the client.

Last updated