InteractiveFrameworks

Custom Decorators

Write a parameter decorator that hands handlers the current user or one of its properties, and compose role metadata and a guard into one @Auth() decorator.

What you'll learn

  • Create a parameter decorator with createParamDecorator and use its data argument to select a property
  • Compose several decorators into one named decorator with applyDecorators
  • Place every stage of the request pipeline in order: middleware, guards, interceptors, pipes, handler, interceptors, filters

Nest is built on decorators, and it lets you write your own. Two kinds matter in everyday code. Parameter decorators pull something out of the request so handlers do not have to, and composed decorators bundle several decorators into one so a rule is written once. Both exist to keep handlers short and to keep the same thing from being spelled out in twenty places.

Parameter decorators

The built-in ones each map to a part of the Express request: @Req(), @Res(), @Param(key?), @Body(key?), @Query(key?), @Headers(name?), @Ip(), @Session(). A custom one is made with createParamDecorator, which takes a factory function:

export const HeaderValue = createParamDecorator((name: string, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest();
  return request.headers[name.toLowerCase()];
});
@Get()
findAll(@HeaderValue('x-request-id') requestId: string) { ... }

The factory receives two things. data is whatever was passed to the decorator at the use site, 'x-request-id' above, and it is undefined when the decorator is used without an argument. ctx is the ExecutionContext from the last two lessons. Whatever the factory returns becomes the argument's value. That is the whole mechanism: @Body('name') is nothing more than a factory that returns request.body[data].

A decorator that reads request.user replaces const user = req.user in every handler, and it removes @Req() from handlers, which makes them easier to test: a handler that takes a user object can be called with one in a test, while a handler that takes a request needs a fake request. Pipes work on custom decorators as on built-in ones: @User(new ValidationPipe({ validateCustomDecorators: true })).

Composing decorators

A handler that needs the same stack every time (metadata, a guard, a status code, a header) accumulates a stack of decorators, and every handler that needs the stack repeats it. applyDecorators bundles them into one:

export function Cacheable(seconds: number) {
  return applyDecorators(HttpCode(200), Header('Cache-Control', `public, max-age=${seconds}`));
}
@Get()
@Cacheable(60)
findAll() { ... }

applyDecorators takes any number of class or method decorators and returns one that applies them all. The stack is defined once, and changing it changes every handler that uses it. It also gives the stack a name: @Auth('admin') says what a handler needs, where @Roles(['admin']) plus @UseGuards(RolesGuard) says how it is checked.

Where it sits

Custom decorators are not a pipeline stage. A parameter decorator's factory runs when the handler's arguments are being built, after guards and alongside pipes; a composed decorator is just its parts, which run wherever those parts run.

Your task

The starter carries the middleware and the guard from lesson 8: x-user: ada is an admin, x-user: bob a reader. CatsController already uses both decorators, but they do nothing yet.

  1. Implement User in user.decorator.ts. @User() gives the handler the request's user object, and @User('name') gives one property of it. With no user on the request, both give undefined.
  2. Implement Auth(...roles) in auth.decorator.ts so that @Auth('admin') on a handler requires the role and binds the guard, using the Roles decorator and RolesGuard from lesson 8.

The tests create cats as two users, list each user's own cats, and delete as a reader and as an admin.

When it fails

  • createdBy is an object: the factory ignores data and returns the whole user for @User('name').
  • createdBy is missing: the factory returns undefined, probably because it read data where the property should be, or the request was taken from ctx without switchToHttp().
  • Anonymous requests to /cats/mine are a 500: the factory reads a property of a user that is not there. Use optional chaining.
  • Deleting as bob answers 200: Auth does not bind the guard, or does not pass the roles on.

The pipeline, complete

You have now met every stage Nest runs around a handler. In the order a request meets them:

  1. Middleware runs first, before routing, and sees a path and a method.
  2. Guards know the handler and decide whether the request may reach it.
  3. Interceptors run before the handler and again after it, with its result.
  4. Pipes transform and validate the arguments just before the handler.
  5. The handler runs, with the arguments the decorators built.
  6. Interceptors see the result on its way out.
  7. Exception filters shape the response when anything above threw.

Every stage is a class Nest constructs, so every stage can inject providers, and every stage is bound with a decorator on a handler, on a controller, or once for the application. That is the whole model. The next course, Fundamentals, is about the providers underneath it.

Remember

  • createParamDecorator((data, ctx) => value): data is the decorator's argument, the return value is the parameter.
  • Custom parameter decorators keep @Req() out of handlers.
  • applyDecorators(...) bundles decorators into one, and names the bundle.
Stuck? Show a hint

user.decorator.ts: take the request from ctx.switchToHttp().getRequest(); its user may be undefined, so read a property with optional chaining, and only when data was given. auth.decorator.ts: applyDecorators with Roles(roles) and UseGuards(RolesGuard), in that order or the other.