docs/site/Express-middleware.md
Express is the most popular web framework for Node.js developers. As quoted below from the Express web site, middleware are the basic building blocks for Express applications.
Express is a routing and middleware web framework that has minimal functionality of its own: An Express application is essentially a series of middleware function calls.
LookBack 4 leverages Express behind the scenes for its REST server implementation. We decided to not expose middleware capabilities to users while we pursue an elegant and non-invasive way to fit Express middleware into the LoopBack 4 programming model nicely. Meanwhile, we have received various requests and questions from our users on how to use Express middleware with LoopBack 4 or migrate their usage of Express middleware from LoopBack 3 to LoopBack 4.
The following use cases are identified to allow Express middleware to work with LoopBack 4:
@loopback/authentication contributes an
Authenticate action. It should be possible to build an extension module for
Helmet to provide better protection for
LoopBack.Let's start with a few examples to illustrate how we can bring Express middleware to LoopBack applications with minimal effort.
Express middleware can now be plugged into the REST sequence with an
InvokeMiddleware action being injected to the default sequence.
The custom sequence class below invokes two Express middleware
(helmet and
morgan) handler functions as the first
step.
{% include code-caption.html content="src/sequence.ts" %}
import helmet from 'helmet'; // For security
import morgan from 'morgan'; // For http access logging
const middlewareList: ExpressRequestHandler[] = [
helmet({}), // options for helmet is fixed and cannot be changed at runtime
morgan('combined', {}), // options for morgan is fixed and cannot be changed at runtime
];
export class MySequence extends DefaultSequence {
async handle(context: RequestContext): Promise<void> {
try {
const {request, response} = context;
// `this.invokeMiddleware` is an injected function to invoke a list of
// Express middleware handler functions
const finished = await this.invokeMiddleware(context, middlewareList);
if (finished) {
// The http response has already been produced by one of the Express
// middleware. We should not call further actions.
return;
}
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (error) {
this.reject(context, error);
}
}
}
InvokeMiddleware actionsWhile the explicit Express middleware invocation is easy and simple, there are some limitations.
src/sequence.ts.
It's not easy to plug in a new middleware.src/sequence.ts.We provide another option to make invocation of Express middleware more flexible
and extensible. The InvokeMiddleware actions within the sequence can discover
registered middleware and invoke them in a chain.
We first register middleware against the default or a named chain using APIs
from RestApplication. It can happen in the constructor of an application.
{% include code-caption.html content="src/application.ts" %}
import morgan from 'morgan';
import {ApplicationConfig} from '@loopback/core';
import {RestApplication} from '@loopback/rest';
export class MyApplication extends RestApplication {
constructor(config: ApplicationConfig) {
this.expressMiddleware(
morgan,
{}, // default config
{
// Allow configuration to be injected to allow dynamic changes to
// morgan logging by configuring `middleware.morgan` to a new value
injectConfiguration: 'watch',
key: 'middleware.morgan',
},
);
}
}
The LoopBack global and local interceptors now also serve as an avenue to attach middleware logic to specific points of controller invocations, such as global, class, or method levels.
There are a few options to wrap an Express middleware module into an LoopBack 4 interceptor.
Let's walk through a few examples:
If the Express middleware only exposes the handler function without a factory or
a single instance is desired, use toInterceptor.
import {toInterceptor} from '@loopback/rest';
import morgan from 'morgan';
const morganInterceptor = toInterceptor(morgan('combined'));
When the Express middleware module exports a factory function that takes an
optional argument for configuration, use createInterceptor.
import {createInterceptor} from '@loopback/rest';
import helmet, {IHelmetConfiguration} from 'helmet';
const helmetConfig: IHelmetConfiguration = {};
const helmetInterceptor = createInterceptor(helmet, helmetConfig);
If the Express middleware module does not expose a factory function conforming
to the ExpressMiddlewareFactory signature, a wrapper can be created. For
example:
import morgan from 'morgan';
// Register `morgan` express middleware
// Create a middleware factory wrapper for `morgan(format, options)`
const morganFactory = (config?: morgan.Options) => morgan('combined', config);
It's often desirable to allow dependency injection of middleware configuration
for the middleware. We can use defineInterceptorProvider to simplify
definition of such provider classes.
import {defineInterceptorProvider} from '@loopback/rest';
import helmet, {IHelmetConfiguration} from 'helmet';
const helmetProviderClass = defineInterceptorProvider<IHelmetConfiguration>(
helmet,
{}, // default config
);
Alternatively, we can create a subclass of
ExpressMiddlewareInterceptorProvider.
import {config} from '@loopback/core';
import {
ExpressMiddlewareInterceptorProvider,
createMiddlewareInterceptorBinding,
} from '@loopback/rest';
import helmet, {IHelmetConfiguration} from 'helmet';
class HelmetInterceptorProvider extends ExpressMiddlewareInterceptorProvider<IHelmetConfiguration> {
constructor(@config() helmetConfig?: IHelmetConfiguration) {
super(helmet, helmetConfig);
}
}
The provider class can then be registered to the application. For example, the
code below can be used in the constructor of your Application subclass.
const binding = createMiddlewareInterceptorBinding(HelmetInterceptorProvider);
this.add(binding);
With the ability to wrap Express middleware as LoopBack 4 interceptors, we can
use the same programming model to register middleware as global interceptors or
local interceptors denoted by @intercept decorators at class and method
levels.
The middleware interceptor function can be directly referenced by @intercept.
import morgan from 'morgan';
const morganInterceptor = toInterceptor(morgan('combined'));
class MyController {
@intercept(morganInterceptor)
hello(msg: string) {
return `Hello, ${msg}`;
}
}
It's also possible to bind the middleware to a context as a local or global interceptor.
import helmet, {IHelmetConfiguration} from 'helmet';
const binding = registerExpressMiddlewareInterceptor(
app,
helmet,
{},
{
// As a global interceptor
global: true,
key: 'interceptors.helmet',
},
);
For a bound local interceptor, the binding key can now be used with
@intercept.
@intercept('interceptors.helmet')
class MyController {
hello(msg: string) {
return `Hello, ${msg}`;
}
}
lb4 interceptor command to create interceptors for Express middlewareThe lb4 interceptor can be used to generate a skeleton implementation of
global or local interceptors. We can update the generated code to plug in
Express middleware. For example, to add
helmet as the security middleware:
lb4 interceptor
? Interceptor name: Helmet
? Is it a global interceptor? Yes
? Group name for the global interceptor: ('') middleware
create src/interceptors/helmet.interceptor.ts
update src/interceptors/index.ts
Interceptor Helmet was created in src/interceptors/
Let's update `src/interceptors/helmet.interceptor.ts:
import {config, globalInterceptor} from '@loopback/core';
import helmet, {IHelmetConfiguration} from 'helmet';
import {ExpressMiddlewareInterceptorProvider} from '@loopback/rest';
@globalInterceptor('middleware', {tags: {name: 'Helmet'}})
export class MorganInterceptor extends ExpressMiddlewareInterceptorProvider<IHelmetConfiguration> {
constructor(
@config()
options: IHelmetConfiguration = {
hidePoweredBy: true,
},
) {
super(helmet, options);
}
}
Express allows HTTP verbs to be used to set up routes, such as
app.post('/hello', ...). See http://expressjs.com/en/4x/api.html#app.METHOD.
To allow a similar usage in LoopBack, we can create an Express router and register it to LoopBack as follows:
import {ExpressRequestHandler, Router} from '@loopback/rest';
const handler: ExpressRequestHandler = async (req, res, next) => {
res.send(req.path);
};
const router = Router();
router.post('/greet', handler);
router.get('/hello', handler);
const binding = server.expressMiddleware('middleware.express.greeting', router);
In some cases, your Express middleware may need to access LoopBack's
RequestContext to resolve certain bindings. This can be done using
getMiddlewareContext function to access the MIDDLEWARE_CONTEXT property of
the Express request object, which is set up by LoopBack when the
RequestContext is instantiated.
import {SecurityBindings} from '@loopback/security';
import {
RequestContext,
getMiddlewareContext,
Request,
Response,
} from '@loopback/rest';
function myExpressHandler(
req: Request,
res: Response,
next: express.NextFunction,
) {
const reqCtx = getMiddlewareContext<RequestContext>(req);
// Now you have access to the LoopBack RequestContext
const currentUser = reqCtx.getSync(SecurityBindings.USER);
}
Middleware and Interceptor are key concepts that allow Express middleware
into LoopBack seamlessly. Please read the following pages to better understand
the architecture.