Skip to main content

HTTP Interceptors

The interceptors are very similar in functionality to controllers, but they do not create routes, they are attached to existing routes. Multiple interceptors can work on a single route, launching one after another. Interceptors are analogous to middleware in ExpressJS, but interceptors can use DI. Additionally, interceptors can work before and after the controller's operation.

Given that interceptors do the same job that controllers can do, you can work without interceptors. But in this case, you will have to call various services in the controllers much more often.

Typically, interceptors are used to automate standard processing, such as:

  • parsing the body of the request or headers;
  • validation of the request;
  • collecting and logging various application metrics;
  • caching;
  • etc.

Interceptors can be centrally connected or disconnected without changing the method code of the controllers to which they are attached. Like controllers, interceptors can be singletons or non-singletons. Unlike singletons, non-singletons have access to the request-level injector, so they can invoke request-level services. On the other hand, singletons are created at the route level - services are available to them at the route, module, or application level.

HTTP request processing scheme

Non-singletons

HTTP request processing has the following workflow:

  1. Ditsmod creates an instance of PreRouter at the application level.
  2. PreRouter uses the router to search for the request handler according to the URI.
  3. If the request handler is not found, PreRouter issues a 501 error.
  4. If a request handler is found, Ditsmod creates a provider instance with the HttpFrontend token at the request level, places it first in the queue of interceptors, and automatically calls it. By default, this interceptor is responsible for setting values for providers with QUERY_PARAMS and PATH_PARAMS tokens.
  5. If there are guards in the current route, then by default InterceptorWithGuards is run immediately after HttpFrontend.
  6. Other interceptors may be launched next, depending on whether the previous interceptor in the queue will launch them.
  7. If all interceptors have worked, Ditsmod starts HttpBackend, which is instantiated at the request level. By default, HttpBackend runs directly the controller method responsible for processing the current request.

So, the approximate order of processing the request is as follows:

  1. PreRouter;
  2. HttpFrontend;
  3. InterceptorWithGuards;
  4. other interceptors;
  5. HttpBackend;
  6. controller method.

Given that starting from PreRouter and to the controller method, a promise is passed, then the same promise will be resolved in the reverse order - from the controller method to PreRouter.

As PreRouter, HttpFrontend, InterceptorWithGuards, and HttpBackend instances are created using DI, you can replace them with your own version of the respective classes. For example, if you don't just want to send a 501 status when the required route is missing, but also want to add some text or change headers, you can substitute PreRouter with your own class.

Each call to the interceptor returns Promise<any>, and it eventually leads to a controller method tied to the corresponding route. This means that in the interceptor you can listen for the result of promise resolve, which returns the method of the controller. However, at the moment (Ditsmod v2.0.0), HttpFrontend and HttpBackend by default ignores everything that returns the controller or interceptors, so listening to the resolution of a promise can be useful only for collecting metrics.

On the other hand, with DI you can easily replace HttpFrontend or HttpBackend with your own interceptors to take into account the return value of the controller method. One of the variants of this functionality is implemented in the @ditsmod/return module.

Singleton

A singleton interceptor works very similarly to a non-singleton interceptor, except that it does not use an injector at the request level. The workflow with his participation differs in points 4 and 7, because a single interceptor instance is created at the route level:

  1. Ditsmod creates an instance of PreRouter at the application level.
  2. PreRouter uses the router to search for the request handler according to the URI.
  3. If the request handler is not found, PreRouter issues a 501 error.
  4. If a request handler is found, Ditsmod uses a provider instance with the HttpFrontend token at the route level, places it first in the interceptor queue, and automatically invokes it. By default, this interceptor is responsible for setting pathParams and queryParams values for SingletonRequestContext.
  5. If there are guards in the current route, then by default SingletonInterceptorWithGuards is run immediately after HttpFrontend.
  6. Other interceptors may be launched next, depending on whether the previous interceptor in the queue will launch them.
  7. If all interceptors have worked, Ditsmod starts HttpBackend, the instance of which is used at the route level. By default, HttpBackend runs directly the controller method responsible for processing the current request.

Creating an interceptor

Each interceptor should be a class implementing the HttpInterceptor interface and annotated with the injectable decorator:

import { injectable, RequestContext, HttpHandler, HttpInterceptor } from '@ditsmod/core';

@injectable()
export class MyHttpInterceptor implements HttpInterceptor {
intercept(next: HttpHandler, ctx: RequestContext) {
return next.handle(); // Here returns Promise<any>;
}
}

As you can see, the intercept() method has two parameters: the first is the handler instance that calls the next interceptor, and the second is RequestContext (native Node.js request and response objects). If the interceptor needs additional data for its work, it can be obtained in the constructor through DI, as in any service.

Passing interceptor to the injector

The non-singleton interceptor is passed to the injector at the request level using multi-providers with the HTTP_INTERCEPTORS token:

import { HTTP_INTERCEPTORS, featureModule } from '@ditsmod/core';
import { MyHttpInterceptor } from './my-http-interceptor.js';

@featureModule({
// ...
providersPerReq: [{ token: HTTP_INTERCEPTORS, useClass: MyHttpInterceptor, multi: true }],
})
export class SomeModule {}

Transmission of a singleton interceptor occurs in exactly the same way, but at the route, module, or application level:

import { HTTP_INTERCEPTORS, featureModule } from '@ditsmod/core';
import { MyHttpInterceptor } from './my-http-interceptor.js';

@featureModule({
// ...
providersPerApp: [{ token: HTTP_INTERCEPTORS, useClass: MyHttpInterceptor, multi: true }],
})
export class SomeModule {}

In this case, the interceptor is passed at the application level, but keep in mind that if you also pass interceptors at lower levels, this interceptor will be ignored. This is how multi-providers work.

In this case, the interceptors are passed in the module's metadata. They can also be passed in the controller metadata. This means that interceptors can either work for all controllers in the module without exception, or only for a specific controller. If you only need to add interceptors to individual routes within controllers, you can do so with extensions (this is how interceptors for parsing the request body are added).