Here are 20 Fastify interview questions with model answers, written for senior Node.js engineers who build APIs with Fastify v5. They cover plugins and encapsulation, decorators, the request lifecycle and hooks, JSON Schema validation and serialization, TypeScript type providers, error handling, testing, production operations and the upgrade from Fastify v4. General Node.js topics such as the event loop, streams and worker threads are on our Node.js interview questions page. At Ryz, senior Node.js engineers are sourced by recruiters, assessed in structured NTRVSTA AI interviews, and reviewed by recruiters before and after. The AI scores are one input. People make the decisions.
Fastify v5 was released in September 2024 and requires Node.js 20 or later. Fastify v4 reached the end of its long-term support on 30 June 2025, and a v6 line is in alpha. Many production services still carry v4 habits, so tell the candidate which version you run. Someone who can name the v5 breaking changes from memory has usually done the upgrade.
The strongest signal in a Fastify interview is how a candidate thinks about structure: which plugin owns which decorator, hook and error handler, and which schema guards each route on the way in and on the way out. Ask them to sketch the plugin tree of a service they built. Then have them add a route live.
Every fastify.register() call creates a child context. Decorators, hooks, error handlers and routes added inside it are visible to that context and its descendants, but not to its parent or siblings. This lets you build an app as a tree of independent plugins: an admin plugin can add an auth hook that only applies to admin routes, and a public plugin is unaffected.
What a strong answer shows: They describe the plugin tree as a scoping tool, not just a file-splitting convention, and can predict where a hook applies.
import fp from "fastify-plugin";
async function dbPlugin(app, opts) {
const pool = await createPool(opts.url);
app.decorate("db", pool);
app.addHook("onClose", async () => pool.end());
}
export default fp(dbPlugin, { name: "db", fastify: "5.x" });
Because of encapsulation, the decorator only exists in the child context. Wrapping the plugin with fastify-plugin tells Fastify to skip the new context, so the decorator, hooks and error handler are added to the parent and shared with siblings. Use it for shared infrastructure such as database pools, auth and config. Leave feature plugins encapsulated.
What a strong answer shows: They declare dependencies with the name and dependencies options and close resources in an onClose hook.
Fastify uses avvio to load plugins in the order they are registered, one at a time, and a plugin's children finish loading before the next sibling starts. Awaiting app.register() loads that plugin immediately, and app.after() runs code once everything registered so far is loaded. app.ready() resolves when the whole tree is loaded, and listen() calls it for you.
What a strong answer shows: They know why accessing a decorator right after an un-awaited register() fails, and they use @fastify/autoload for large route trees.
decorate, decorateRequest and decorateReply add properties to the instance, request or reply. Declaring request and reply properties up front keeps object shapes stable, which helps V8 optimize. Fastify v5 forbids decorating requests or replies with a reference type such as an object or array, because the same reference would be shared by every request. Declare the property with no value, or null, and set it per request in a hook, or use a function or getter.
app.decorateRequest("user", null);
app.addHook("onRequest", async (request) => {
request.user = await authenticate(request.headers.authorization);
});
What a strong answer shows: They explain why a shared object across requests is a data leak waiting to happen, not just a v5 error message.
onRequest runs first, before the body is parsed, which makes it the place for cheap auth checks and rate limiting. Then preParsing can transform the raw body stream, preValidation runs after parsing but before schema validation, and preHandler runs after validation, right before the handler. On the way out, preSerialization can change the payload before it is serialized, onSend sees the serialized payload, and onResponse runs after the response is sent, which suits metrics. onError, onTimeout and onRequestAbort cover failures and dropped clients.
What a strong answer shows: They put each concern in the earliest hook that has the data it needs, and know onError is for logging, not for changing the response.
Register the auth hook in an encapsulated plugin that only contains protected routes, and register public routes outside it. For finer control, use route-level onRequest or preHandler hooks, or read request.routeOptions.config in a shared hook so a route can set config: { public: true }. Use onRequest when the token is in a header, so unauthenticated requests never pay for body parsing.
What a strong answer shows: They prefer structure over allowlists of URLs, and know that in v5 the route data lives on request.routeOptions.
app.get("/report", async (request, reply) => {
generateReport(request.query.id, (err, pdf) => {
if (err) return reply.code(500).send(err);
reply.type("application/pdf").send(pdf);
});
});
The async function resolves with undefined before the callback runs, so Fastify logs an error that the handler's promise resolved with undefined, and anything thrown inside the callback bypasses Fastify's error handling. In an async handler, either return the value, or call reply.send() and return reply to tell Fastify to wait. Better still, promisify generateReport and return its result.
What a strong answer shows: They know the "return a value or return reply" rule and avoid mixing callbacks into async handlers.
app.post("/orders", {
schema: {
body: {
type: "object",
required: ["sku", "quantity"],
additionalProperties: false,
properties: {
sku: { type: "string", minLength: 1 },
quantity: { type: "integer", minimum: 1, maximum: 100 }
}
},
response: {
201: {
type: "object",
properties: { id: { type: "string" }, status: { type: "string" } }
}
}
}
}, async (request, reply) => {
const order = await createOrder(request.body);
return reply.code(201).send(order);
});
Schemas for body, querystring, params and headers are compiled with Ajv when the app starts, so validation is fast at request time. Invalid requests get a 400 with the validation details. Fastify's default Ajv options coerce types, apply defaults and remove additional properties where a schema sets additionalProperties: false.
What a strong answer shows: They know the defaults, especially coercion of query strings and removal of extra properties, and turn them off deliberately when they are wrong for the API.
Fastify compiles response schemas with fast-json-stringify, which serializes much faster than JSON.stringify. It also only outputs properties the schema lists, so a field such as passwordHash on a database row never reaches the client by accident. Response schemas can be keyed by status code, including ranges such as "4xx".
What a strong answer shows: They see the response schema as a security control as much as a speed gain, and they test that new fields are added to it deliberately.
Add them once with app.addSchema({ $id: "Order", ... }) and reference them with { $ref: "Order#" }. Shared schemas follow encapsulation like everything else, so add them in a plugin that is visible where they are used. The same schemas can feed @fastify/swagger to generate OpenAPI documentation, so the docs match what the server enforces.
What a strong answer shows: One source of truth for validation, serialization, types and docs.
Use a type provider. With @fastify/type-provider-typebox, you write schemas with TypeBox, call app.withTypeProvider<TypeBoxTypeProvider>(), and request.body, request.query and the reply payload are typed from the same schema object that validates them. json-schema-to-ts and community Zod providers work the same way. Fastify v5 separates validator and serializer types, so request and response types can come from different libraries.
What a strong answer shows: They avoid writing an interface and a schema separately, which drift apart.
Throw errors from handlers and hooks, or return rejected promises, and let a setErrorHandler turn them into responses. Error handlers are encapsulated, so a plugin can format its own errors. Use @fastify/error or @fastify/sensible for typed HTTP errors with status codes, check error.validation to shape validation failures, and add setNotFoundHandler for 404s. Log the full error server-side and never return stack traces.
What a strong answer shows: They keep handlers free of try/catch-to-response boilerplate and know that 5xx errors are logged at error level by default while 4xx errors are logged at info.
import { test } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../src/app.js";
test("POST /orders rejects a zero quantity", async (t) => {
const app = await buildApp({ db: fakeDb() });
t.after(() => app.close());
const res = await app.inject({
method: "POST",
url: "/orders",
payload: { sku: "A1", quantity: 0 }
});
assert.equal(res.statusCode, 400);
});
app.inject() sends a fake HTTP request through the full lifecycle without opening a port, so tests are fast and cover hooks, validation and serialization. Export a buildApp factory that takes dependencies as options, so tests can pass fakes, and close the app after each test.
What a strong answer shows: They separate building the app from starting the server, and test through inject rather than calling handlers directly.
Fix all v4 deprecation warnings first, because v5 removed every one of them. The common changes are: Node.js 20 or later; full JSON Schema required for body, querystring, params and response schemas, including type; listen() only accepts an options object; reply.redirect(url, code) with the URL first; a custom logger goes in loggerInstance instead of logger; route data moved to request.routeOptions; no reference types in request and reply decorators; and plugins can no longer mix the async and callback styles. reply.getResponseTime() became reply.elapsedTime.
What a strong answer shows: A staged plan: clear deprecations on v4, upgrade Node.js, then bump Fastify and the @fastify/* plugins together, because plugin majors track Fastify majors.
Look at its signature. An async plugin function that also accepts and calls done mixed the two styles, which v5 rejects. Also check that the plugin's fastify version range in fastify-plugin metadata allows 5.x, and that its own dependencies have v5-compatible releases.
What a strong answer shows: They read the plugin's registration code instead of pinning the old version and moving on.
Fastify uses Pino, a fast JSON logger. Enable it with logger: true or options, use request.log inside handlers so every line carries the request ID, and redact secrets with Pino's redact option, for example req.headers.authorization. Ship JSON to your log pipeline and use pino-pretty only in development. In v5, pass an existing Pino instance through loggerInstance.
What a strong answer shows: Request-scoped logging, redaction by default and no pretty-printing in production.
On SIGTERM, call app.close(). It stops accepting connections, answers new requests on open connections with 503 by default, closes idle keep-alive connections, waits for in-flight requests, and runs preClose and onClose hooks so plugins can close pools and queues. Add a hard timeout so a stuck request can't block the deploy, and fail the readiness probe first so the load balancer stops sending traffic.
What a strong answer shows: They tie shutdown to the orchestrator: readiness first, drain, close resources, then exit.
trustProxy, so request.ip, request.protocol and request.host come from forwarding headers, set to the specific proxy hops rather than true when possible. The keep-alive timeout, which must be longer than the load balancer's idle timeout to avoid intermittent 502s. bodyLimit, which defaults to 1 MiB, raised only on routes that need it. Rate limiting with @fastify/rate-limit keyed on the real client IP.
What a strong answer shows: They have debugged a 502 from mismatched keep-alive timeouts or a rate limiter that saw only the proxy's IP.
Routing uses find-my-way, a radix tree router, schemas are compiled ahead of time, and response serialization uses compiled serializers. The overhead per request is low, so in a real service the time goes to your own code: synchronous CPU work that blocks the event loop, large JSON payloads, N+1 database queries and slow downstream calls. Use @fastify/under-pressure to shed load when the event loop lags, and measure with autocannon and a profiler before optimizing.
What a strong answer shows: They don't credit the framework for performance their own code loses, and measure before tuning.
Use OpenTelemetry with the Fastify instrumentation, now maintained by the Fastify team as @fastify/otel, alongside HTTP and database instrumentations. Fastify v5 also publishes diagnostic channel events, which tracing tools can subscribe to without wrapping internals. Propagate trace context to downstream calls and include the trace ID in logs.
What a strong answer shows: They connect traces, logs and metrics by request, and know instrumentation must load before the app's modules.
fastify-plugin, so encapsulation does nothing.reply.send() into async handlers without returning reply.inject.Run a 90-minute pairing session on a prepared repo: a Fastify v5 service with an orders API, a Postgres plugin and a few tests using inject. Seed it with problems: an auth hook registered in the wrong context so one admin route is public, a route that returns a row including an internal field, a decorator initialized with a shared object, an async handler that sends from a callback, and no graceful shutdown. Ask the candidate to fix what matters most, explaining priorities as they go.
If you need engineers who already think this way, hire senior Node.js and Fastify developers through Ryz. They come from the top 1% of candidates we interview, work on your team and in your repos, and work on US business hours. Our vetting process shows how we evaluate them, and you can adapt our Node.js developer job description for a Fastify posting.
Interview for v5, the supported line, and accept answers in v4 vocabulary. The core ideas are the same: plugins, encapsulation, hooks and schemas. A candidate who knows v4 well can move to v5 quickly, because most changes are signatures, full JSON Schemas and decorators.
Express experience transfers for HTTP and Node.js fundamentals, but Fastify's model is different: encapsulated plugins, lifecycle hooks and schema-driven validation and serialization. Ask one question on encapsulation and one on schemas to see whether an Express candidate adjusts quickly.
Six to eight. Pick two on plugins and encapsulation, one on hooks, two on schemas and serialization, and one on errors or testing. Add the v5 upgrade question if you are migrating, then spend the remaining time on a short live exercise.
Questions we didn't answer? Email info@ryzlabs.com.