Tutorial: Routing

Routing

Defining Routes

Flitter routes are defined within files inside the app/routing/routers/ folder. For those familiar with MVC frameworks, each file is a route group (which can have its own prefix), and middleware can be applied to individual routes, groups, or globally.

Flitter comes with a default routes file, app/routing/routers/index.routes.js which shows the structure of a Flitter routes file:

const index = {
    prefix: '/',

    middleware: [
        // 'MiddlewareName',
    ],

    get: {
        // e.g. 'middleware::MiddlewareName'
        // e.g. 'controller::ControllerName.method'
        '/': [ 'controller::Home.welcome' ],
    },

    post: {

    },
}

module.exports = exports = index

  • prefix defines the route prefix
    • e.g. if you define /home in the file with a prefix of /user, then the final route will be /user/home.
  • middleware is an array of middleware to be applied, in order, to all routes in the file.
    • You should refer to middleware by their canonical name, unprefixed. That is, if you want to apply the app/routing/middleware/util/Config.middleware.js middleware, you would refer to it as util:Config.
  • get defines routes for the GET method, and post for the POST method
    • These should be objects where the key is the route's URI, and the value is an array of canonical names referring to handlers to be applied, in order, when the route is handled. Route-specific middleware should be included here.
    • Because this array can contain both controller methods and middleware handlers, you must refer to these items by their fully-qualified canonical names. That is, the name with the type prefix. So, if you wanted to apply the same app/routing/middleware/util/Config.middleware.js file to a specific route, you would call it middleware::util:Config.
    • Likewise, every route should contain at least one controller method to send the response. Say you wanted to call the welcome method on the app/controllers/Home.controller.js file, you would refer to it as controller::Home.welcome.
    • Supported HTTP verb groups are get, post, put, delete, copy, patch.

When Flitter starts, it loads each routes file and creates an Express Router for each one, with the specified prefix. Group middleware is applied to the router, and then each route is registered.

Recall that route specific information is not available in the route group level. That is, a middleware applied to an entire file cannot access route specific information like request.params. However, it can access this information from the route-level handlers.

Creating Custom Route Files

Custom route files can be created and placed in the app/routing/routers/ folder. As long as they conform to the form required, they will be loaded by Flitter when the application is started. You can create a new route file from the template using a ./flitter command:

./flitter new router file_name

Which will create the file routing/routers/file_name.routes.js.

Middleware

Middleware in Flitter is defined in the app/routing/middleware/ directory. The middleware's class should contain a method test() which takes 3 variables: the Express request, the Express response, and the function to be called to continue execution of the Flitter stack. Here's an example from flitter-auth (Flitter's first-party auth provider):

/*
 * UserOnly Middleware
 * -------------------------------------------------------------
 * Allows the request to proceed if there's an authenticated user
 * in the session. Otherwise, redirects the user to the login page
 * of the default provider.
 */
const Middleware = require('libflitter/middleware/Middleware')
class UserOnly extends Middleware {
    async test(req, res, next, args = {}){
        if ( req.is_auth ) return next()
        else {
            // If not signed in, save the target url so we can redirect back here after auth
            req.session.auth.flow = req.originalUrl
            return res.redirect('/auth/login')
        }
        
    }
}

module.exports = UserOnly

Here, the middleware checks if the request.is_auth flag has been set. This is set by flitter-auth's global middleware when a valid user has been authenticated. If this flag is set, then we allow the request to continue. Otherwise, we redirect to the login page. The next() call is very important because it allows the request to continue being processed by the defined handlers for that route.

Middleware must end in one of two ways. It either sends a response itself, or it calls next() and allows the next handler to deal with the request. Failing to do one of these things will result in the user's browser hanging while it waits for a response.

Using Middleware

Middleware can be applied in three ways: globally, per group, or per route. Global middleware is applied to every request Flitter handles, group middleware is applied to all routes in a given routes file, and route middleware is applied to a single route.

Middleware loaded by Flitter should be referenced using its canonical name. All three of the places middleware is applied in Flitter accept references in this format. The reason for this is that Flitter applies processing and dependency injection to the middleware, so it should always be instantiated by the framework, not the user.

Global Middleware

Middleware can be applied globally by adding it to the array of middleware in app/routing/Middleware.js. The middleware in this file is applied in order to every request Flitter handles. An example:

// app/routing/Middleware.js
const Middleware = [
    'util:RouteLogger',
    'auth:Utility',
    'my:custom:MiddlewareName',
]

module.exports = exports = Middleware

Here, the util:RouteLogger, auth:Utility, and my:custom:MiddlewareName middlewares are applied. This is done, in order, for every request that is handled by Flitter. Note that, because only middleware is defined in this file, you should use the unqualified canonical name (i.e. the name without the middleware:: prefix).

Group Middleware

Middleware can be applied to all routes in a group. In Flitter, each routes file is a group. Therefore, middleware can be applied to all routes in a given file by adding it to the middleware array in that file. For example:

// app/routing/routers/index.routes.js
const index = {
    prefix: '/',
    middleware: [
        'util:HomeLogger',
    ],
    get: {
        '/': [ 'controller::Home.welcome' ],
    },
}

module.exports = exports = index

Here, the HomeLogger middleware will be applied, in order, to all routes specified in the file, regardless of request method. So, navigating to the / route, the request would pass through global middleware first, then util:HomeLogger, then the route specific handler.

Route Middleware

Finally, middleware can be applied to individual routes by adding the middleware to the array of canonical names in a given routes file. For example:

// app/routing/routers/index.routes.js
const index = {
    prefix: '/',
    middleware: [],
    get: {
        '/': [
            'middleware::util:HomeLogger',
            'controller::Home.welcome',
        ],
    },
}

module.exports = exports = index

Here, the util:HomeLogger middleware will be applied first, then the Home.welcome controller method will be called. Because you can specify either middleware or controller handlers in this array, you must include the qualified canonical name (i.e. the name with the middleware:: or controller:: prefix).

Creating Custom Middleware

Custom middleware files can be created and placed in the app/routing/middleware/ folder. As long as they conform to the form required, they will be loaded by Flitter when the application is started, and they can be accessed by canonical name within the framework. You can create a new middleware file from the template using a ./flitter command:

./flitter new middleware subdirectory:file_name

Which will create the file app/routing/routers/subdirectory/file_name.routes.js:

const Middleware = require('libflitter/middleware/Middleware')

/*
 * file_name Middleware
 * -------------------------------------------------------------
 * Put some description here!
 */
class file_name extends Middleware {

    /*
     * Run the middleware test.
     * This method is required by all Flitter middleware.
     * It should either call the next function in the stack,
     * or it should handle the response accordingly.
     */
    test(req, res, next, args = {}){
        // Do stuff here

        /*
         * Call the next function in the stack.
         */
        next()
    }
}

module.exports = exports = file_name

Next: Views & Static Assets