Page Contents

Introduction

In LoopBack 3, a Remote hook enables you to execute a function before or after a remote method is called by a client:

  • beforeRemote() runs before the remote method.
  • afterRemote() runs after the remote method has finished successfully.
  • afterRemoteError() runs after the remote method has finished with an error.

Hooks can be configured to run for one or more methods using a wildcard specifier. In general, there are three kinds of hook scopes:

  • Global hooks are executed for every remote method.

    These hooks are registered on the application object using a ** wildcard. For example:

    app.beforeRemote('**', handlerFn);
    
  • Model level hooks are executed for every remote method of a given model class.

    These hooks are typically registered on the application object using {model-name}.** wildcard. For example, to run a hook for all methods of the User model:

    app.beforeRemote('User.**', handlerFn);
    
  • Method level hooks are executed only for a single method of a given model class.

    These hooks are typically registered on the model class. For example, to run a hook whenever User.login is called remotely:

    User.beforeRemote('login', handlerFn);
    

LoopBack 4 provides Interceptors feature to enable application developers to implement similar functionality.

  • Global interceptors are executed for every request handled by a LoopBack 4 controller method or a LoopBack 4 route handler. They correspond to LoopBack 3 global hooks.
  • Class level interceptors are executed for requests handled by the given Controller class. They correspond to LoopBack 3 model level hooks.
  • Method level interceptors are executed only for request handled by the given controller method. They correspond to LoopBack 3 method level hooks.

In the following sections, we will first explain how to rewrite a LoopBack 3 hook implementation to a LoopBack 4 interceptor implementation. Then we show how to create interceptors replacing LoopBack 3 remoting hooks registered at global, model (class) level and method level.

Rewriting a LoopBack 3 hook to a LoopBack 4 interceptor

Let’s take a look at a typical implementation of a LoopBack 4 interceptor:

async function intercept(
  invocationCtx: InvocationContext,
  next: () => ValueOrPromise<InvocationResult>,
) {
  try {
    // Add pre-invocation logic here
    const result = await next();
    // Add post-invocation logic here
    return result;
  } catch (err) {
    // Add error handling logic here
    throw err;
  }
}

Every interceptor contains three parts, each corresponding to one of the remoting hooks:

  • Code from a beforeRemote hook belongs to the first section that’s executed before the target method is invoked.
  • Code from an afterRemote hook belongs to the second section.
  • Code from an afterRemoteError hook belongs to the third section inside the catch block.

Example: from a hook to an interceptor

Consider the following set of LoopBack 3 remoting hooks implementing a very simple request logger. We are registering the hooks at global level to keep this example simple, but the same approach works for model level and method level hooks too.

app.beforeRemote('**', function logBefore(ctx, next) {
  console.log('About to invoke a method.');
  next();
});

app.afterRemote('**', function logAfter(ctx, next) {
  console.log('Method finished.');
  next();
});

app.afterRemoteError('**', function logAfterError(ctx, next) {
  console.log('Method failed: ', ctx.error);
});

These three hooks can be converted into a single interceptor in LoopBack 4.

try {
  // Add pre-invocation logic here
  // Code from beforeRemote hooks go here
  console.log('About to invoke a method.');
  const result = await next();
  // Add post-invocation logic here
  // Code from afterRemote hooks go here
  console.log('Method finished.');
  return result;
} catch (err) {
  // Add error handling logic here
  // Code from afterRemoteError hooks go here
  console.log('Method failed: ', err);
  throw err;
}

The example above is intentionally simple and does not access any data from the context argument. See Accessing context data for instructions on how to map LoopBack 3 context properties to LoopBack 4.

Migrating global hooks

Global remoting hooks should be rewritten to global interceptors. You can use Interceptor generator CLI to create the necessary infrastructure for each hook.

First, run lb4 interceptor to create a new interceptor. Pick an interceptor name (e.g. GlobalLogger) and make sure to ask the generator to scaffold a global interceptor. You can pick an empty interceptor group ('') when prompted.

$ lb4 interceptor
? Interceptor name: GlobalLogger
? Is it a global interceptor? Yes

Global interceptors are sorted by the order of an array of group names
bound to ContextBindings.GLOBAL_INTERCEPTOR_ORDERED_GROUPS.
See https://loopback.io/doc/en/lb4/Interceptor.md#order-of-invocation-for-interceptors.

? Group name for the global interceptor: ('')
  create src/interceptors/global-logger.interceptor.ts
  update src/interceptors/index.ts

Interceptor GlobalLogger was created in src/interceptors/

Next, open the generated interceptor file and replace the default implementation of intercept method with the code adopted from your original LoopBack 3 hooks, as explained in Rewriting a LoopBack 3 hook to a LoopBack 4 interceptor. See Accessing context data for instructions on how to map LoopBack 3 context properties to LoopBack 4.

Migrating model level hooks

Model level hooks should be rewritten to class interceptors. You can use Interceptor generator CLI to create the necessary infrastructure for each hook.

First, run lb4 interceptor to create a new interceptor. Pick an interceptor name (e.g. ProductLogger) and make sure to ask the generator to scaffold a non-global interceptor.

$ lb4 interceptor
? Interceptor name: ProductLogger
? Is it a global interceptor? No
   create src/interceptors/product-logger.interceptor.ts
   update src/interceptors/index.ts

Interceptor ProductLogger was created in src/interceptors/

Next, open the generated interceptor file and replace the default implementation of intercept method with the code adopted from your original LoopBack 3 hooks, as explained in Rewriting a LoopBack 3 hook to a LoopBack 4 interceptor. See Accessing context data for instructions on how to map LoopBack 3 context properties to LoopBack 4.

Finally, register the new interceptor to be invoked for all methods of the target controller class.

For example:

import {inject, intercept} from '@loopback/core';
// ...

@intercept(ProductLoggerInterceptor.BINDING_KEY)
export class ProductController {
  // ...
}

Migrating method level hooks

Method level hooks should be rewritten to method interceptors. You can use Interceptor generator CLI to create the necessary infrastructure for each hook.

First, run lb4 interceptor to create a new interceptor. Pick an interceptor name (e.g. CreationLogger) and make sure to ask the generator to scaffold a non-global interceptor.

$ lb4 interceptor
? Interceptor name: CreationLogger
? Is it a global interceptor? No
   create src/interceptors/creation-logger.interceptor.ts
   update src/interceptors/index.ts

Interceptor CreationLogger was created in src/interceptors/

Next, open the generated interceptor file and replace the default implementation of intercept method with the code adopted from your original LoopBack 3 hooks, as explained in Rewriting a LoopBack 3 hook to a LoopBack 4 interceptor. See Accessing context data for instructions on how to map LoopBack 3 context properties to LoopBack 4.

Finally, register the new interceptor to be invoked for the selected methods of the target controller class.

For example:

import {inject, intercept} from '@loopback/core';
// ...

export class ProductController {
  // ...

  @intercept(CreationLoggerInterceptor.BINDING_KEY)
  @post('/todos', {
    responses: {
      '200': {
        description: 'Todo model instance',
        content: {'application/json': {schema: getModelSchemaRef(Todo)}},
      },
    },
  })
  async create(/*...*/): Promise<Product> {
    // ...
  }
}

Accessing context data

In LoopBack 3, a remoting hook receives a context object providing:

  • Transport-specific data like the HTTP request & response objects.
  • Transport-independent data, for example the array of input arguments for the remote method and the result of remote method invocation.

In LoopBack 4, interceptors receive an instance of InvocationContext class. This class is similar to LoopBack 3 remoting context in the sense that it holds different pieces of data related to method invocation, but it’s also different in the way how these properties are accessed.

LoopBack 3 uses regular property access, for example ctx.req is used to access the current HTTP request.

In LoopBack 4, InvocationContext is a part of Context hierarchy starting from application level context, including request level context and finally the invocation context.

Invocation-specific properties can be accessed directly on the invocation context object, see InvocationContext API docs for the complete list. Other context properties (e.g. the current HTTP request) are usually accessed via Dependency Injection.

For example, the current HTTP request can be accessed as follows:

// other imports skipped for brevity
import {Request, RestBindings} from '@loopback/rest';

export class LoggingInterceptor implements Provider<Interceptor> {
  constructor(@inject(RestBindings.Http.REQUEST) private request: Request) {}

  // ...

  async intercept(
    invocationCtx: InvocationContext,
    next: () => ValueOrPromise<InvocationResult>,
  ) {
    console.log('%s %s', this.request.method, this.request.url);
    return next();
  }
}

Context properties

The following table maps commonly used LoopBack 3 context properties to their LoopBack 4 counter-parts.

LoopBack 3 LoopBack 4
ctx.req inject RestBindings.Http.REQUEST
ctx.res inject RestBindings.Http.RESPONSE
ctx.args invocationCtx.args
ctx.result the value returned by await next()
ctx.error use catch to receive errors thrown by await next()
ctx.req.accessToken see Accessing the current user below
ctx.methodString invocationCtx.targetName

Modifying request parameters

It is possible to modify arguments passed to the controller method before the method is invoked.

Consider the following LoopBack 3 remoting hook executed for Comment.create method to add postedFromIpAddress field to every new comment posted:

Comment.beforeRemote('create', function (ctx, next) {
  ctx.args[0].postedFromIpAddress = ctx.req.remoteAddress;
  next();
});

This remoting hook can be rewritten to a LoopBack 4 interceptor as follows:

class AddRemoteAddressInterceptor implements Provider<Interceptor> {
  constructor(@inject(RestBindings.Http.REQUEST) private request: Request) {}

  // ...

  async intercept(
    invocationCtx: InvocationContext,
    next: () => ValueOrPromise<InvocationResult>,
  ) {
    invocationCtx.args[0].postedFromIpAddress =
      this.request.socket.remoteAddress;
    return next();
  }
}

Modifying the response

You can transform the value returned by await next() before returning it from the interceptor, thus modifying the final response returned to the client.

Consider the following example from LoopBack 3 documentation:

// prevent password hashes from being sent to clients
Customer.afterRemote('**', function (ctx, user, next) {
  if (ctx.result) {
    if (Array.isArray(ctx.result)) {
      ctx.result.forEach(function (result) {
        result.unsetAttribute('password');
      });
    } else {
      ctx.result.unsetAttribute('password');
    }
  }

  next();
});

This remoting hook can be rewritten to a LoopBack 4 interceptor as follows:

async function intercept(
  invocationCtx: InvocationContext,
  next: () => ValueOrPromise<InvocationResult>,
) {
  const result = await next();
  if (Array.isArray(result)) {
    result.forEach(it => delete it.password);
  } else {
    delete result.password;
  }
  return result;
}

Rejecting requests

In order to reject a request and return an error HTTP response, just throw an error from your interceptor. As explained in Handling errors in controllers, you can use one of HttpErrors constructors to control the HTTP status code of the response.

Consider the following LoopBack 3 example rejecting requests from non-local addresses:

Customer.beforeRemote('**', function (ctx, next) {
  if (ctx.req.remoteAddress !== '127.0.0.1') {
    next(new HttpErrors.Forbidden());
  } else {
    next();
  }
});

This remoting hook can be rewritten to a LoopBack 4 interceptor as follows:

class AddRemoteAddressInterceptor implements Provider<Interceptor> {
  constructor(@inject(RestBindings.Http.REQUEST) private request: Request) {}

  // ...

  async intercept(
    invocationCtx: InvocationContext,
    next: () => ValueOrPromise<InvocationResult>,
  ) {
    if (this.request.socket.remoteAddress !== '127.0.0.1') {
      throw new HttpErrors.Forbidden();
    }
    return next();
  }
}

Accessing the current user

LoopBack 4 does not provide authentication layer out of the box; it relies on extensions to implement authentication features. Please refer to the documentation for your authentication component to learn how to access data about the current user.

If you are using the official LoopBack Authentication Extension, then you can access profile of the currently authenticated user via the binding SecurityBindings.USER.

Please note the value for the binding may be bound later in the request handling cycle, therefore it’s best to use @inject.getter() to defer resolution of the value until the intercept method is invoked.

Consider the following LoopBack 3 example printing a warning whenever an anonymous (unauthenticated) request is made:

Customer.beforeRemote('*.save', function (ctx, unused, next) {
  if (!ctx.req.accessToken) {
    console.warn('anonymous request!');
  }
  next();
});

This remoting hook can be rewritten to a LoopBack 4 interceptor as follows:

class WarnAnonymousInterceptor implements Provider<Interceptor> {
  constructor(
    @inject.getter(SecurityBindings.USER, {optional: true})
    private getCurrentUser: Getter<UserProfile>,
  ) {}

  async intercept(
    invocationCtx: InvocationContext,
    next: () => ValueOrPromise<InvocationResult>,
  ) {
    const currentUser = await this.getCurrentUser();
    if (!currentUser) {
      console.warn('anonymous request!');
    }
    return next();
  }
}

Please note the current user must be injected as an optional value, because the value is available for authenticated endpoints only. In a typical application, not all endpoints are decorated with the @authenticate decorator. If you leave the injection as required (that’s the default), then you will get a binding error when an unauthenticated endpoint is accessed. See Using the Authentication Decorator for more details.