onRequest, preHandler, preValidation, onSend
Request Hooks
Per-request hooks run at each step of the lifecycle — authentication, parsing, response shaping all live here.
What you'll learn
- Register an onRequest hook for early checks
- Use preHandler for authorization
- Use onSend to mutate the outgoing payload
A request flows through a fixed series of stages. At each stage you can run a hook to inspect, mutate, or short-circuit the request.
The Order
onRequest— earliest; no body parsed yet, ideal for rate limits and quick rejectspreParsing— wrap the body stream (rare)preValidation— body present, schema not yet enforcedpreHandler— body validated, run authorization herehandler— your routepreSerialization— mutate the value before it becomes JSONonSend— modify the serialized payload or headersonResponse— request finished; logging and metrics
Auth in preHandler
app.addHook('preHandler', async (req, reply) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) throw app.httpErrors.unauthorized();
const user = await verifyToken(token);
req.user = user;
}); Throwing inside any hook short-circuits the request — the handler never runs, and the error flows to your error handler.
Per-Route Hooks
app.post('/admin/wipe', {
preHandler: requireAdmin,
handler: async () => wipeDatabase(),
}); Per-route hooks compose with the scope-level hooks above them — both run, in order.
Avoid Heavy Work Where It Doesn’t Belong
Anything that needs the parsed body goes in preValidation or later. Anything that should run
even on validation failures (rate limit, audit log) goes in onRequest. Authorization checks
should be in preHandler so a 400 from invalid input doesn’t waste a JWT verify.