Skip to content

Instantly share code, notes, and snippets.

@bajtos
Last active August 29, 2015 14:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bajtos/e7eaba736ff096916b71 to your computer and use it in GitHub Desktop.
Save bajtos/e7eaba736ff096916b71 to your computer and use it in GitHub Desktop.
LoopBack Middleware docs

Working with middleware

INTERNAL NOTE: the features described in this document are supported since loopback 2.8.0, loopback-boot 2.4.0 and yo loopback 1.6.0.

Overview

Middleware refers to functions executed when HTTP requests are made to REST endpoints. Since LoopBack is based on Express, LoopBack middleware is the same as Express middleware.

Configuring middleware in loopback applications

Projects scaffolded via slc loopback keep all middleware configuration in the file server/middleware.json

Example:

{
  "initial:before": {
    "loopback#favicon": {
    }
  },
  "initial": {
    "compression": {
      "enabled": true,
      "params": {
        "threshold": 512
      }
    },
  },
  "routes": {
    "loopback#status": {
      "paths": "/"
    }
  },
  "final": {
    "loopback#urlNotFound": {
    }
  },
  "final:after": {
    "errorhandler": {
    }
  }
}

The configuration can be further customized through middleware.local.js/json and middleware.{env}.js/json.

Format of middleware.json

The configuration file has the following format:

  • At the top level, the keys define named phases in which the middleware functions are executed, e.g. "initial" or "final". The phases are run in the same order as they are listed in the file.

  • Each phase section contains a map of middleware to run in this phase, keyed by a path to the file exporting the middleware constructor. For example, "loopback/server/middleware/favicon" or "compression".

  • A middleware section contains additional middleware properties as described below.

Phases

Every loopback application has the following predefined phases:

  • initial - Register middleware that must run among the first.

  • session - Prepare the session object.

  • auth - Handle authentication and authorization.

  • parse - Parse the request body.

  • routes - Express routes implementing your application logic.

    Any middleware registered via the express API app.use, app.route, app.get (and other HTTP verbs) is run at the beginning of this phase.

    Use this phase also for sub-apps like loopback/server/middleware/rest or loopback-explorer.

  • files - Serve static assets (requests are hitting the file system here).

  • final - Deal with errors and requests for unknown URLs.

The phases listed in middleware.json are merged with the predefined phases in such way that both orderings are preserved.

For example, below is a middleware.json file defining a new phase "log" that should be run after "parse" and before "routes":

{
  "parse": {},
  "log": {},
  "routes": {}
}

Each phase provides three sub-phases: before, main, and after. The before/after phases are encoded in the phase name as a postfix. For example, the "initial" phase executes middleware registered in "initial:before" first, then in "initial" and at last "initial:after".

Middleware registered with the same phase name is invoked in the order in which it was registered. However, you should not rely on such order. Always explicitly order the middleware using appropriate phases when the order matters.

Path to middleware source file

There are multiple ways of specifying the path to the file exporting the middleware constructor:

  • A name of the module that is installed in the project, e.g. compression.

  • A path to a file located inside a module, e.g. loopback/server/middleware/rest

  • A path relative to middleware.json, e.g. ./middleware/custom

  • An absolute path (not recommended).

Additionally, there is also a shorthand format {module}#{fragment} available. Paths in this format are resolved in the following order:

  • The fragment is a property exported by the module, e.g. loopback#favicon is resolved to require('loopback').favicon.

  • The fragment is a file in module's server/middleware directory, e.g. require('loopback/server/middleware/favicon')

  • The fragment is a file in modules' middleware directory, e.g. require('loopback/middleware/favicon')

Middleware configuration

The following properties can be specified in each middleware section:

  • enabled - whether the middleware should be registered at all, true by default. You can override this property in environment-specific files, for example to disable certain middleware when running in production.

  • params - parameters to pass to the middleware constructor. Most middleware constructors expects a single "options" object, in that case the params value is this object:

    "compression": {
      "params": { "threshold": 512 }
    }

    To configure constructors accepting more than one parameter, you can provide an array of arguments:

    "morgan": {
      "params": ["dev", { "buffer": true } ]
    }
  • paths - This parameter is a string that specifies the REST endpoint that will trigger the middleware. If you don't provide the parameter, then the middleware will trigger on all routes. In addition to a literal string, route can be a path matching pattern, a regular expression, or an array including all these types. For more information, see the Express documentation for app.use().

In order to support project-relative paths, the params object is scanned for string values starting with the prefix $!. Such values are interpreted as paths relative to the file middleware.json and resolved into absolute paths. This feature is typically used to specify paths to directories containing static assets.

An example configuring a static middleware serving the client directory located in project's root:

{
  "files": {
    "loopback#static": {
      "params": "$!../client"
    }
  }
}

Built-in middleware

LoopBack provides convenience middleware for some commonly-used Express/Connect middleware, as described in the following table.

middleware id code accessor Express/Connect middleware
loopback#favicon loopback.favicon() serve-favicon
loopback#static loopback.static() serve-static

In order to simplify migration from LoopBack 1.x and Express 3.x, LoopBack provides also middleware that used to be a part of Express 3.x. Developers are encouraged to modify their applications to load this middleware directly via require() and do not rely on loopback's compatibility layer.

LoopBack middleware Express/Connect middleware
... ...

(Copy the table from Express middleware and remove favicon entry, as it was already described above.)

Code-first configuration

The middleware can be configured directly from your javascript code too. See API docs for app.middleware, app.middlewareFromConfig and app.defineMiddlewarePhases.

Example:

var loopback = require('loopback');
var morgan = require('morgan');
var errorhandler = require('error-handler');

var app = loopback();

// (configure datasources and models)

app.middleware('routes:before', morgan('dev'));
app.middleware('final', errorhandler());
app.middleware('routes', loopback.rest());

// this will be invoked in `routes` phase
app.get('/status', function(req, res, next) {
  res.json({ running: true });
});

Defining routes

A route is a special case of express middleware, the handler function is expected to send back the response and omit the call of next().

While it is possible to use app.use and app.middleware to define routes, the more common approach is to use the API app.route, app.get, app.post, etc.

Example:

app.get('/', function(req, res, next) {
  res.send('hello from `get` route');
});

app.post('/', function(req, res, next) {
  res.send('hello from `post` route')
});

NOTE: Handlers registered through Express API are always executed at the beginning of the routes phase.

Caveats

There are some things to look out for when using middleware, mostly to do with middleware declaration order. Be aware of the order of your middleware registration when using "catch-all" routes. For example:

...
app.get('/', function(req, res, next) {
  res.send('hello from `get` route');
});
app.use(function(req, res, next) {
  console.log('hello world from "catch-all" route');
  next();
});
app.post('/', function(req, res, next) {
  res.send('hello from `post` route')
});
...

In this case, since the GET / middleware ends the response chain, the "catch-all" middleware is never triggered when a get request is made. However, when you make a POST request to /, the "catch-all" route is triggered because it is declared before the post route. Doing a POST will show the console message from both the "catch-all" route and the POST / route.

Defining a new middleware

Handler function

A middleware handler is a function accepting three or four arguments.

Regular handlers

A middleware function with three arguments is called to process the request when there was no error reported by the previous handlers.

function myMiddleware(req, res, next) {
  // ...
}

Error handlers

A middleware function with four arguments is called only when an error was encountered.

function myErrorHandler(err, req, res, next) {
  // ...
}

Handler arguments

(Copy the section "Middleware function parameters" from the current docs)

http://docs.strongloop.com/display/LB/Defining+middleware#Definingmiddleware-Middlewarefunctionparameters

Middleware constructor

Middleware modules typically export a constructor (factory) function that accepts configuraton options and returns a middleware handler function. You must follow this pattern if you want to load your custom middleware via middleware.json.

module.exports = function(options) {
  return function customHandler(req, res, next) {
    // use options to control handler's behaviour
  }
};

Examples

Pre-processing middleware

Use pre-processing middleware to apply custom logic for various endpoints in your application. Do this by registering handler functions to perform certain operations when HTTP requests are made to specific endpoint or endpoints. Always register pre-processing middleware with phases invoked before routes, e.g. initial or parse.

Pre-processing middleware must call next() at the end of the handler function to pass control to the next middleware. If you don't do this, then your application will essentially "freeze." Technically, next() doesn't have to occur at the end of the function (for example, it could occur in an if / else block), but the handler function must call it eventually.

module.exports = function() {
  return function tracker(req, res, next) {
    console.log('Request tracking middleware triggered on %s', req.url);
    var start = process.hrtime();
    res.once('finish', function() {
      var diff = process.hrtime(start);
      var ms = diff[0] * 1e3 + diff[1] * 1e-6;
      console.log('The request processing time is %d ms.', ms);
    });
    next();
  };
};

This middleware tells the server to display the time it takes to process the incoming HTTP request on all application routes.

You can see this middleware in action in using the basic LoopBack application from Getting started with LoopBack (or any standard LoopBack application):

  1. Add the code above to /server/middleware/tracker.js.

  2. Edit (or create) the file /server/middleware.json and register the new middleware in the "initial" phase:

    {
      "initial": {
        "./middleware/tracker": {}
      }
    }
  3. Start the application with slc run.

  4. Load http://localhost:3000/ in your browser.

In the console, you will see (for example):

...
Request tracking middleware triggered on /.
The request processing time is 4.281957 ms. //your results will vary

Error-handling middleware

Use error-handling middleware to deal with request errors. While you are free to register any number of error-handling middleware, be sure to register them in the "final" phase.

LoopBack registers two error-handling middleware by default: urlNotFound and errorhandler. The urlNotFound middleware converts all requests that reach the middleware into an Error object with status 404, so that 404 error responses are consistent with "usual" error responses.

errorhandler is the middleware provided by errorhandler module, it is be available as express.errorHandler in Express version 3.

Example of a custom error processing middleware:

module.exports = function() {
  return function logError(err, req, res, next) {
    console.log('ERR', req.url, err);
  };
};
  1. Add the code above to /server/middleware/log-error.js.

  2. Edit (or create) the file /server/middleware.json and register the new middleware in the "final" phase:

    {
      "final": {
        "./middleware/log-error": {}
      }
    }
  3. Start the application with slc run.

  4. Load http://localhost:3000/url-does-not-exist in your browser.

Upgrading existing projects

Before the concept of middleware phases was introduced, most middleware was registered in server/server.js:

// Set up the /favicon.ico
app.use(loopback.favicon());

// request pre-processing middleware
app.use(loopback.compress());

// -- Add your pre-processing middleware here --

// boot scripts mount components like REST API
boot(app, __dirname);

// -- Mount static files here--
// All static middleware should be registered at the end, as all requests
// passing the static middleware are hitting the file system
// Example:
var path = require('path');
app.use(loopback.static(path.resolve(__dirname, '../client')));

// Requests that get this far won't be handled
// by any middleware. Convert them into a 404 error
// that will be handled later down the chain.
app.use(loopback.urlNotFound());

// The ultimate error handler.
app.use(loopback.errorHandler());

In order to migrate your project to the new layout leveraging middleware phases, you need to move the middleware configuration from server/server.js to server/middleware.json.

Middleware added after boot

Error handlers

Replace the line app.use(loopback.errorHandler()) with the following entry in the JSON file:

{
  "final:after": {
    "errorhandler": {}
  }
}

404 handler

Replace the line app.use(loopback.urlNotFound()) with the following entry in the JSON file:

{
  "final": {
    "loopback#urlNotFound": {}
  }
}

Static middleware

Middleware serving static assets (files) should be registered in files phase. Prefix the relative path to assets with $! in order to get it resolved into an absolute path.

For example, the following line in server/server.js

app.use(loopback.static(path.resolve(__dirname, '../client')));

should be replaced with this middleware entry:

{
  "files": {
    "loopback#static": {
      "params": "#!../client"
    }
  }
}

Other post-boot handlers

If your server/server.js file registers any other middleware after calling boot(app, __dirname), you need to move that registration to server/middleware.json. Use the appropriate phase (final:before, final or final:after) to get the middleware executed at the right time.

Middleware added before boot

The middleware registered before boot(app, __dirname) is usually pre-processing the requests.

LoopBack provides multiple phases for such middleware, you need to pick up the right phase for each of them.

  • favicon should be called as the very first middleware, even before request loggers. Register it in initial:before phase.

  • compression should be called very early in the middleware chain to enable response compression as soon as possible. Register it in initial phase.

  • express-session (a.k.a. loopback.session()) should be registered in session phase.

  • loopback#token (a.k.a. loopback.token()) belongs to auth phase.

  • morgan (a.k.a. loopback.logger()) should usually go to initial phase.

  • Request body parsers like bodyParser#json belong to parse phase.

Middleware registered from boot scripts

Any middleware installed via app.use(fn) is added to the routes phase. Call app.middleware(phase, fn) to add the middleware to a different phase.

Example:

module.exports = function(app) {
  app.middleware('initial', mylogger());
};

Example of migrated files

After you have applied the steps outlined above to the example server/server.js listed at the beginning of this guide, you should end up with the following two files:

server/server.js

// boot scripts mount components like REST API
boot(app, __dirname);

server/middleware.json

{
  "initial:before": {
    "loopback#favicon": {}
  },
  "initial": {
    "compression": {}
  },
  "session": {
  },
  "auth": {
  },
  "parse": {
  },
  "routes": {
  },
  "files": {
    "loopback#static": {
      "params": "$!../client"
    }
  },
  "final": {
    "loopback#urlNotFound": {}
  },
  "final:after": {
    "errorhandler": {}
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment