Hooks
A hook is a function Fastify calls at a certain stage of a request's or application's life. Hooks replace the middleware of other frameworks: they check access, add headers, write logs.
Request stages
| Hook | When it runs | Example task |
|---|---|---|
onRequest | Right after the request arrives | Start a timer, check a token |
preValidation | Before schema validation | Prepare data |
preHandler | Before the route handler | Check permissions |
onSend | Before the reply is sent | Change the reply, add a header |
onResponse | After the reply is sent | Record statistics |
Application hooks: onReady — after all plugins have loaded, onClose — when the server stops.
onRequest
Add a hook to the src/plugins/timing.js plugin from the previous lesson:
// src/plugins/timing.js
import fp from 'fastify-plugin';
async function timing(app) {
app.decorateRequest('startTime', 0);
app.addHook('onRequest', async (request) => {
request.startTime = Date.now();
});
}
export default fp(timing);Now every request has its start time in request.startTime.
onSend
The onSend hook gets a third argument — the ready reply body. Add it to the same plugin:
// src/plugins/timing.js
app.addHook('onSend', async (request, reply, payload) => {
reply.header('x-response-time', `${Date.now() - request.startTime}ms`);
return payload;
});curl -i http://localhost:3000/posts shows the header x-response-time: 2ms. The hook must return payload — whatever it returns goes to the client.
A hook for one route
A hook can be attached to a single route instead of the whole application — in the route options:
// src/routes/posts.js
async function checkAdminHeader(request, reply) {
if (request.headers['x-admin'] !== 'yes') {
return reply.code(403).send({ error: 'Access denied' });
}
}
app.delete('/posts/:id', { preHandler: checkAdminHeader }, async (request, reply) => {
return reply.code(204).send();
});DELETE /posts/1 without the x-admin: yes header returns 403, and the handler does not run. A real token check is shown in the authentication lesson.
onClose
onClose runs on app.close() — that is where resources are released:
// src/plugins/timing.js
app.addHook('onClose', async () => {
app.log.info('Server is stopping');
});When the server stops through app.close(), this line appears in the log. In the database lesson the connection is closed here.
Scope
A hook added in a plugin without fastify-plugin affects only that plugin's routes. This way you can turn on a check for one group of routes.
Common mistake
If a hook sent a reply with reply.send() but did not return reply, Fastify keeps going and calls the route handler. In an async hook write return reply.code(403).send(...), as in the example above, or throw an error with throw.