Files
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
Custom Decorators
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.
- Implement
Userinuser.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 giveundefined. - Implement
Auth(...roles)inauth.decorator.tsso that@Auth('admin')on a handler requires the role and binds the guard, using theRolesdecorator andRolesGuardfrom 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
createdByis an object: the factory ignoresdataand returns the whole user for@User('name').createdByis missing: the factory returnsundefined, probably because it readdatawhere the property should be, or the request was taken fromctxwithoutswitchToHttp().- Anonymous requests to
/cats/mineare a 500: the factory reads a property of a user that is not there. Use optional chaining. - Deleting as bob answers 200:
Authdoes 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:
- Middleware runs first, before routing, and sees a path and a method.
- Guards know the handler and decide whether the request may reach it.
- Interceptors run before the handler and again after it, with its result.
- Pipes transform and validate the arguments just before the handler.
- The handler runs, with the arguments the decorators built.
- Interceptors see the result on its way out.
- 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):datais 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.
Press Run tests to start the app. Its log appears here.Graded endpoints
@User('name') hands the handler the current user's name, stored as createdBy
A second cat, owned by ada
Only bob's cat
No user on the request: the decorator yields undefined, nothing matches, and nothing crashes
A handler without @Auth is open to anyone
@Auth('admin') requires a role and there is no user: the guard from lesson 8 answers 401
@Auth('admin') bundles the role metadata and the guard: a reader is refused
An admin may delete
Only Kitty is left