Overview

Interceptors are reusable functions to provide aspect-oriented logic aroundmethod invocations. There are many use cases for interceptors, such as:

  • Add extra logic before / after method invocation, for example, logging ormeasuring method invocations.
  • Validate/transform arguments
  • Validate/transform return values
  • Catch/transform errors, for example, normalize error objects
  • Override the method invocation, for example, return from cacheThe following diagram illustrates how interceptors can be applied to theinvocation of a method on the controller class.

Interceptors

Basic use

Interceptors on controllers

Interceptors are supported for public controller methods (including both staticand prototype) and handler functions for REST routes.

Controller methods decorated with @intercept are invoked with appliedinterceptors for corresponding routes upon API requests.

  1. import {intercept} from '@loopback/context';
  2. @intercept(log) // `log` is an interceptor function
  3. export class OrderController {
  4. @intercept('caching-interceptor') // `caching-interceptor` is a binding key
  5. async listOrders(userId: string) {
  6. // ...
  7. }
  8. }

NOTE: log and 'caching-interceptor' are illustrated inExample interceptors.

It’s also possible to configure global interceptors that are invoked beforemethod level interceptors. For example, the following code registers a globalcaching-interceptor for all methods.

  1. app
  2. .bind('caching-interceptor')
  3. .toProvider(CachingInterceptorProvider)
  4. .apply(asInterceptor);

Global interceptors are also executed for route handler functions without acontroller class. See an example inRoute Handler.

Create a proxy to apply interceptors

Aproxycan be created from the target class or object to apply interceptors. This isuseful for the case that a controller declares dependencies of repositories orservices and would like to allow repository or service methods to beintercepted.

  1. import {createProxyWithInterceptors} from '@loopback/context';
  2. const proxy = createProxyWithInterceptors(controllerInstance, ctx);
  3. const msg = await proxy.greet('John');

There is also an asProxyWithInterceptors option for binding resolution ordependency injection to return a proxy for the class to apply interceptors whenmethods are invoked.

  1. class DummyController {
  2. constructor(
  3. @inject('my-controller', {asProxyWithInterceptors: true})
  4. public readonly myController: MyController,
  5. ) {}
  6. }
  7. ctx.bind('my-controller').toClass(MyController);
  8. ctx.bind('dummy-controller').toClass(DummyController);
  9. const dummyController = await ctx.get<DummyController>('dummy-controller');
  10. const msg = await dummyController.myController.greet('John');
  11. );

Or:

  1. const proxy = await ctx.get<MyController>('my-controller', {
  2. asProxyWithInterceptors: true,
  3. });
  4. const msg = await proxy.greet('John');

Please note synchronous methods (which don’t return Promise) are converted toreturn ValueOrPromise (synchronous or asynchronous) in the proxy so thatinterceptors can be applied. For example,

  1. class MyController {
  2. name: string;
  3. greet(name: string): string {
  4. return `Hello, ${name}`;
  5. }
  6. async hello(name: string) {
  7. return `Hello, ${name}`;
  8. }
  9. }

The proxy from an instance of MyController has the AsyncProxy<MyController>type:

  1. {
  2. name: string; // the same as MyController
  3. greet(name: string): ValueOrPromise<string>; // the return type becomes `ValueOrPromise<string>`
  4. hello(name: string): Promise<string>; // the same as MyController
  5. }

The return value of greet now has two possible types:

  • string: No async interceptor is applied
  • Promise<string>: At least one async interceptor is applied

Use invokeMethod to apply interceptors

To explicitly invoke a method with interceptors, use invokeMethod from@loopback/context. Please note invokeMethod is used internally byRestServer for controller methods.

  1. import {Context, invokeMethod} from '@loopback/context';
  2. const ctx: Context = new Context();
  3. ctx.bind('name').to('John');
  4. // Invoke a static method
  5. let msg = await invokeMethod(MyController, 'greetStaticWithDI', ctx);
  6. // Invoke an instance method
  7. const controller = new MyController();
  8. msg = await invokeMethod(controller, 'greetWithDI', ctx);

Please note that invokeMethod internally uses invokeMethodWithInterceptorsto support both injection of method parameters and application of interceptors.

Apply interceptors

Interceptors form a cascading chain of handlers around the target methodinvocation. We can apply interceptors by decorating methods/classes with@intercept. Please note @intercept does NOT return a new method wrappingthe target one. Instead, it adds some metadata instead and such information isused by invokeMethod or invokeWithMethodWithInterceptors functions totrigger interceptors around the target method. The original method stays intact.Invoking it directly won’t apply any interceptors.

@intercept

Syntax: @intercept(…interceptorFunctionsOrBindingKeys)

The @intercept decorator adds interceptors to a class or its methods includingstatic and instance methods. Two flavors are accepted:

  • An interceptor function
  1. class MyController {
  2. @intercept(log) // Use the `log` function
  3. greet(name: string) {
  4. return `Hello, ${name}`;
  5. }
  6. }
  • A binding key that can be resolved to an interface function
  1. class MyController {
  2. @intercept('name-validator') // Use the `name-validator` binding
  3. async helloWithNameValidation(name: string) {
  4. return `Hello, ${name}`;
  5. }
  6. }
  7. // Bind options and provider for `NameValidator`
  8. ctx.bind('valid-names').to(['John', 'Mary']);
  9. ctx.bind('name-validator').toProvider(NameValidator);

Method level interceptors

A public static or prototype method on a class can be decorated with@intercept to attach interceptors to the target method. Please noteinterceptors don’t apply to protected or private methods.

Static methods

  1. class MyControllerWithStaticMethods {
  2. // Apply `log` to a static method
  3. @intercept(log)
  4. static async greetStatic(name: string) {
  5. return `Hello, ${name}`;
  6. }
  7. // Apply `log` to a static method with parameter injection
  8. @intercept(log)
  9. static async greetStaticWithDI(@inject('name') name: string) {
  10. return `Hello, ${name}`;
  11. }
  12. }

Prototype methods

  1. class MyController {
  2. // Apply `logSync` to a sync instance method
  3. @intercept(logSync)
  4. greetSync(name: string) {
  5. return `Hello, ${name}`;
  6. }
  7. // Apply `log` to a sync instance method
  8. @intercept(log)
  9. greet(name: string) {
  10. return `Hello, ${name}`;
  11. }
  12. // Apply `log` as a binding key to an async instance method
  13. @intercept('log')
  14. async greetWithABoundInterceptor(name: string) {
  15. return `Hello, ${name}`;
  16. }
  17. // Apply `log` to an async instance method with parameter injection
  18. @intercept(log)
  19. async greetWithDI(@inject('name') name: string) {
  20. return `Hello, ${name}`;
  21. }
  22. // Apply `log` and `logSync` to an async instance method
  23. @intercept('log', logSync)
  24. async greetWithTwoInterceptors(name: string) {
  25. return `Hello, ${name}`;
  26. }
  27. // No interceptors are attached
  28. async greetWithoutInterceptors(name: string) {
  29. return `Hello, ${name}`;
  30. }
  31. // Apply `convertName` to convert `name` arg to upper case
  32. @intercept(convertName)
  33. async greetWithUpperCaseName(name: string) {
  34. return `Hello, ${name}`;
  35. }
  36. // Apply `name-validator` backed by a provider class
  37. @intercept('name-validator')
  38. async greetWithNameValidation(name: string) {
  39. return `Hello, ${name}`;
  40. }
  41. // Apply `logError` to catch errors
  42. @intercept(logError)
  43. async greetWithError(name: string) {
  44. throw new Error('error: ' + name);
  45. }
  46. }

Class level interceptors

To apply interceptors to be invoked for all methods on a class, we can use@intercept to decorate the class. When a method is invoked, class levelinterceptors (if not explicitly listed at method level) are invoked beforemethod level ones.

  1. // Apply `log` to all methods on the class
  2. @intercept(log)
  3. class MyControllerWithClassLevelInterceptors {
  4. // The class level `log` will be applied
  5. static async greetStatic(name: string) {
  6. return `Hello, ${name}`;
  7. }
  8. // A static method with parameter injection
  9. @intercept(log)
  10. static async greetStaticWithDI(@inject('name') name: string) {
  11. return `Hello, ${name}`;
  12. }
  13. // We can apply `@intercept` multiple times on the same method
  14. // This is needed if a custom decorator is created for `@intercept`
  15. @intercept(log)
  16. @intercept(logSync)
  17. greetSync(name: string) {
  18. return `Hello, ${name}`;
  19. }
  20. // Apply multiple interceptors. The order of `log` will be preserved as it
  21. // explicitly listed at method level
  22. @intercept(convertName, log)
  23. async greet(name: string) {
  24. return `Hello, ${name}`;
  25. }
  26. }

Global interceptors

Global interceptors are discovered from the InvocationContext. They areregistered as bindings with interceptor tag. For example,

  1. import {asInterceptor} from '@loopback/context';
  2. app
  3. .bind('interceptors.MetricsInterceptor')
  4. .toProvider(MetricsInterceptorProvider)
  5. .apply(asInterceptor);

Order of invocation for interceptors

Multiple @intercept decorators can be applied to a class or a method. Theorder of invocation is determined by how @intercept is specified. The list ofinterceptors is created from top to bottom and from left to right. Duplicateentries are removed from their first occurrences.

Let’s examine the list of interceptors invoked for each method onMyController, which has a class level log decorator:

  • A static method on the class - greetStatic
  1. @intercept(log)
  2. class MyController {
  3. // No explicit `@intercept` at method level. The class level `log` will
  4. // be applied
  5. static async greetStatic(name: string) {
  6. return `Hello, ${name}`;
  7. }
  8. }

Interceptors to apply: [log]

  • A static method that requires parameter injection: greetStaticWithDI
  1. @intercept(log)
  2. class MyController {
  3. // The method level `log` overrides the class level one
  4. @intercept(log)
  5. static async greetStaticWithDI(@inject('name') name: string) {
  6. return `Hello, ${name}`;
  7. }
  8. }

Interceptors to apply: [log]

  • A prototype method with multiple @intercept - greetSync
  1. @intercept(log)
  2. class MyController {
  3. // We can apply `@intercept` multiple times on the same method
  4. // This is needed if a custom decorator is created for `@intercept`
  5. @intercept(log) // The method level `log` overrides the class level one
  6. @intercept(logSync)
  7. greetSync(name: string) {
  8. return `Hello, ${name}`;
  9. }
  10. }

Interceptors to apply: [log, logSync]

  • A prototype method that preserves the order of an interceptor - greet
  1. @intercept(log)
  2. class MyController {
  3. // Apply multiple interceptors. The order of `log` will be preserved as it
  4. // explicitly listed at method level
  5. @intercept(convertName, log)
  6. async greet(name: string) {
  7. return `Hello, ${name}`;
  8. }
  9. }

Interceptors to apply: [convertName, log]

Global interceptors are invoked before class/method level ones unless they areexplicitly overridden by @intercept.

Global interceptors can be sorted as follows:

  • Tag global interceptor binding with ContextTags.GLOBAL_INTERCEPTOR_GROUP.The tag value will be treated as the group name of the interceptor. Forexample:
  1. app
  2. .bind('globalInterceptors.authInterceptor')
  3. .to(authInterceptor)
  4. .apply(asGlobalInterceptor('auth'));

If the group tag does not exist, the value is default to ''.

  • Control the ordered groups for global interceptors
  1. app
  2. .bind(ContextBindings.GLOBAL_INTERCEPTOR_ORDERED_GROUPS)
  3. .to(['log', 'auth']);

If ordered groups is not bound toContextBindings.GLOBAL_INTERCEPTOR_ORDERED_GROUPS, global interceptors will besorted by their group names alphabetically. Interceptors with unknown groups areinvoked before those listed in ordered groups.

Create your own interceptors

Interceptors can be made available by LoopBack itself, extension modules, orapplications. They can be a function that implements Interceptor signature ora binding that is resolved to an Interceptor function.

Interceptor functions

The interceptor function is invoked to intercept a method invocation with twoparameters:

  • context: the invocation context
  • next: a function to invoke next interceptor or the target method. It returnsa value or promise depending on whether downstream interceptors and the targetmethod are synchronous or asynchronous.
  1. /**
  2. * Interceptor function to intercept method invocations
  3. */
  4. export interface Interceptor {
  5. /**
  6. * @param context - Invocation context
  7. * @param next - A function to invoke next interceptor or the target method
  8. * @returns A result as value or promise
  9. */
  10. (
  11. context: InvocationContext,
  12. next: () => ValueOrPromise<InvocationResult>,
  13. ): ValueOrPromise<InvocationResult>;
  14. }

An interceptor can be asynchronous (returning a promise) or synchronous(returning a value). If one of the interceptors or the target method isasynchronous, the invocation will be asynchronous. The following table show howthe final return type is determined.

InterceptorTarget methodReturn type
asyncasyncpromise
asyncsyncpromise
syncasyncpromise
syncsyncvalue

To keep things simple and consistent, we recommend that interceptors function tobe asynchronous as much as possible.

Invocation context

The InvocationContext object provides access to metadata for the giveninvocation in addition to the parent Context that can be used to locate otherbindings. It extends Context with additional properties as follows:

  • target (object): Target class (for static methods) or prototype/object(for instance methods)
  • methodName (string): Method name
  • args (InvocationArgs, i.e., any[]): An array of arguments
  1. /**
  2. * InvocationContext for method invocations
  3. */
  4. export class InvocationContext extends Context {
  5. /**
  6. * Construct a new instance
  7. * @param parent - Parent context, such as the RequestContext
  8. * @param target - Target class (for static methods) or prototype/object
  9. * (for instance methods)
  10. * @param methodName - Method name
  11. * @param args - An array of arguments
  12. */
  13. constructor(
  14. parent: Context,
  15. public readonly target: object,
  16. public readonly methodName: string,
  17. public readonly args: InvocationArgs, // any[]
  18. ) {
  19. super(parent);
  20. }
  21. }

It’s possible for an interceptor to mutate items in the args array to pass intransformed input to downstream interceptors and the target method.

Logic around next

An interceptor will receive the next parameter, which is a function to executethe downstream interceptors and the target method.

The interceptor function is responsible for calling next() if it wants toproceed with next interceptor or the target method invocation. A typicalinterceptor implementation looks like the following:

  1. async function intercept<T>(
  2. invocationCtx: InvocationContext,
  3. next: () => ValueOrPromise<T>,
  4. ) {
  5. // Pre-process the request
  6. try {
  7. const result = await next();
  8. // Post-process the response
  9. return result;
  10. } catch (err) {
  11. // Handle errors
  12. throw err;
  13. }
  14. }

If next() is not invoked, neither downstream interceptors nor the targetmethod be executed. It’s valid to skip next() if it’s by intention, forexample, an interceptor can fail the invocation early due to validation errorsor return a response from cache without invoking the target method.

Example interceptors

Here are some example interceptor functions:

  • An asynchronous interceptor to log method invocations:
  1. const log: Interceptor = async (invocationCtx, next) => {
  2. console.log('log: before-' + invocationCtx.methodName);
  3. // Wait until the interceptor/method chain returns
  4. const result = await next();
  5. console.log('log: after-' + invocationCtx.methodName);
  6. return result;
  7. };
  • An interceptor to catch and log errors:
  1. const logError: Interceptor = async (invocationCtx, next) => {
  2. console.log('logError: before-' + invocationCtx.methodName);
  3. try {
  4. const result = await next();
  5. console.log('logError: after-' + invocationCtx.methodName);
  6. return result;
  7. } catch (err) {
  8. console.log('logError: error-' + invocationCtx.methodName);
  9. throw err;
  10. }
  11. };
  • An interceptor to convert name arg to upper case:
  1. const convertName: Interceptor = async (invocationCtx, next) => {
  2. console.log('convertName:before-' + invocationCtx.methodName);
  3. invocationCtx.args[0] = (invocationCtx.args[0] as string).toUpperCase();
  4. const result = await next();
  5. console.log('convertName: after-' + invocationCtx.methodName);
  6. return result;
  7. };
  • An provider class for an interceptor that performs parameter validationTo leverage dependency injection, a provider class can be defined as theinterceptor:
  1. /**
  2. * A binding provider class to produce an interceptor that validates the
  3. * `name` argument
  4. */
  5. class NameValidator implements Provider<Interceptor> {
  6. constructor(@inject('valid-names') private validNames: string[]) {}
  7. value() {
  8. return this.intercept.bind(this);
  9. }
  10. async intercept<T>(
  11. invocationCtx: InvocationContext,
  12. next: () => ValueOrPromise<T>,
  13. ) {
  14. const name = invocationCtx.args[0];
  15. if (!this.validNames.includes(name)) {
  16. throw new Error(
  17. `Name '${name}' is not on the list of '${this.validNames}`,
  18. );
  19. }
  20. return next();
  21. }
  22. }
  • A synchronous interceptor to log method invocations:
  1. const logSync: Interceptor = (invocationCtx, next) => {
  2. console.log('logSync: before-' + invocationCtx.methodName);
  3. // Calling `next()` without `await`
  4. const result = next();
  5. // It's possible that the statement below is executed before downstream
  6. // interceptors or the target method finish
  7. console.log('logSync: after-' + invocationCtx.methodName);
  8. return result;
  9. };