Guides development of Fastify Node.js backend servers and REST APIs using TypeScript or JavaScript. Use when building, configuring, or debugging a Fastify application — including defining routes, implementing plugins, setting up JSON Schema validation, handling errors, optimising performance, managing authentication, configuring CORS and security headers, integrating databases, working with WebSockets, and deploying to production. Covers the full Fastify request lifecycle (hooks, serialization, logging with Pino) and TypeScript integration via strip types. Trigger terms: Fastify, Node.js server, REST API, API routes, backend framework, fastify.config, server.ts, app.ts.
95
95%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Use Node.js built-in type stripping (Node.js 22.6+):
# Run TypeScript directly
node --experimental-strip-types app.ts
# In Node.js 23+
node app.ts// package.json
{
"type": "module",
"scripts": {
"start": "node app.ts",
"dev": "node --watch app.ts"
}
}// tsconfig.json for type stripping
{
"compilerOptions": {
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"noEmit": true,
"strict": true
}
}Type your Fastify application:
import Fastify, { type FastifyInstance, type FastifyRequest, type FastifyReply } from 'fastify';
const app: FastifyInstance = Fastify({ logger: true });
app.get('/health', async (request: FastifyRequest, reply: FastifyReply) => {
return { status: 'ok' };
});
await app.listen({ port: 3000 });Use generics to type request parts:
import type { FastifyRequest, FastifyReply } from 'fastify';
interface CreateUserBody {
name: string;
email: string;
}
interface UserParams {
id: string;
}
interface UserQuery {
include?: string;
}
// Type the request with generics
app.post<{
Body: CreateUserBody;
}>('/users', async (request, reply) => {
const { name, email } = request.body; // Fully typed
return { name, email };
});
app.get<{
Params: UserParams;
Querystring: UserQuery;
}>('/users/:id', async (request) => {
const { id } = request.params; // string
const { include } = request.query; // string | undefined
return { id, include };
});
// Full route options typing
app.route<{
Params: UserParams;
Querystring: UserQuery;
Body: CreateUserBody;
Reply: { user: { id: string; name: string } };
}>({
method: 'PUT',
url: '/users/:id',
handler: async (request, reply) => {
return { user: { id: request.params.id, name: request.body.name } };
},
});Use @fastify/type-provider-typebox for runtime + compile-time safety:
import Fastify from 'fastify';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
const app = Fastify().withTypeProvider<TypeBoxTypeProvider>();
const UserSchema = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String({ format: 'email' }),
});
const CreateUserSchema = Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' }),
});
app.post('/users', {
schema: {
body: CreateUserSchema,
response: {
201: UserSchema,
},
},
}, async (request, reply) => {
// request.body is typed as { name: string; email: string }
const { name, email } = request.body;
reply.code(201);
return { id: 'generated', name, email };
});Extend Fastify types with declaration merging:
import Fastify from 'fastify';
// Declare types for decorators
declare module 'fastify' {
interface FastifyInstance {
config: {
port: number;
host: string;
};
db: Database;
}
interface FastifyRequest {
user?: {
id: string;
email: string;
role: string;
};
startTime: number;
}
interface FastifyReply {
sendSuccess: (data: unknown) => void;
}
}
const app = Fastify();
// Add decorators
app.decorate('config', { port: 3000, host: 'localhost' });
app.decorate('db', new Database());
app.decorateRequest('user', null);
app.decorateRequest('startTime', 0);
app.decorateReply('sendSuccess', function (data: unknown) {
this.send({ success: true, data });
});
// Now fully typed
app.get('/profile', async (request, reply) => {
const user = request.user; // { id: string; email: string; role: string } | undefined
const config = app.config; // { port: number; host: string }
reply.sendSuccess({ user });
});Type plugin options and exports:
import fp from 'fastify-plugin';
import type { FastifyPluginAsync } from 'fastify';
interface DatabasePluginOptions {
connectionString: string;
poolSize?: number;
}
declare module 'fastify' {
interface FastifyInstance {
db: {
query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
close: () => Promise<void>;
};
}
}
const databasePlugin: FastifyPluginAsync<DatabasePluginOptions> = async (
fastify,
options,
) => {
const { connectionString, poolSize = 10 } = options;
const db = await createConnection(connectionString, poolSize);
fastify.decorate('db', {
query: (sql: string, params?: unknown[]) => db.query(sql, params),
close: () => db.end(),
});
fastify.addHook('onClose', async () => {
await db.end();
});
};
export default fp(databasePlugin, {
name: 'database',
});Type hook functions:
import type {
FastifyRequest,
FastifyReply,
onRequestHookHandler,
preHandlerHookHandler,
} from 'fastify';
const authHook: preHandlerHookHandler = async (
request: FastifyRequest,
reply: FastifyReply,
) => {
const token = request.headers.authorization;
if (!token) {
reply.code(401).send({ error: 'Unauthorized' });
return;
}
request.user = await verifyToken(token);
};
const timingHook: onRequestHookHandler = async (request) => {
request.startTime = Date.now();
};
app.addHook('onRequest', timingHook);
app.addHook('preHandler', authHook);Create reusable typed schemas:
import type { JSONSchema7 } from 'json-schema';
// Define schema with const assertion for type inference
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
required: ['id', 'name', 'email'],
} as const satisfies JSONSchema7;
// Infer TypeScript type from schema
type User = {
id: string;
name: string;
email: string;
};
app.get<{ Reply: User }>('/users/:id', {
schema: {
response: {
200: userSchema,
},
},
}, async (request) => {
return { id: '1', name: 'John', email: 'john@example.com' };
});Organize types in dedicated files:
// types/index.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
export interface CreateUserInput {
name: string;
email: string;
}
export interface PaginationQuery {
page?: number;
limit?: number;
sort?: string;
}
// routes/users.ts
import type { FastifyInstance } from 'fastify';
import type { User, CreateUserInput, PaginationQuery } from '../types/index.js';
export default async function userRoutes(fastify: FastifyInstance) {
fastify.get<{
Querystring: PaginationQuery;
Reply: { users: User[]; total: number };
}>('/', async (request) => {
const { page = 1, limit = 10 } = request.query;
// ...
});
fastify.post<{
Body: CreateUserInput;
Reply: User;
}>('/', async (request, reply) => {
reply.code(201);
// ...
});
}Create typed route factories:
import type { FastifyInstance, RouteOptions } from 'fastify';
function createCrudRoutes<T extends { id: string }>(
fastify: FastifyInstance,
options: {
prefix: string;
schema: {
item: object;
create: object;
update: object;
};
handlers: {
list: () => Promise<T[]>;
get: (id: string) => Promise<T | null>;
create: (data: unknown) => Promise<T>;
update: (id: string, data: unknown) => Promise<T>;
delete: (id: string) => Promise<void>;
};
},
) {
const { prefix, schema, handlers } = options;
fastify.get(`${prefix}`, {
schema: { response: { 200: { type: 'array', items: schema.item } } },
}, async () => handlers.list());
fastify.get(`${prefix}/:id`, {
schema: { response: { 200: schema.item } },
}, async (request) => {
const item = await handlers.get((request.params as { id: string }).id);
if (!item) throw { statusCode: 404, message: 'Not found' };
return item;
});
// ... more routes
}Keep types simple and practical:
// GOOD - simple, readable types
interface UserRequest {
Params: { id: string };
Body: { name: string };
}
app.put<UserRequest>('/users/:id', handler);
// AVOID - overly complex generic types
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
// AVOID - excessive type inference
type InferSchemaType<T> = T extends { properties: infer P }
? { [K in keyof P]: InferPropertyType<P[K]> }
: never;Use TypeScript for type checking only:
# Type check without emitting
npx tsc --noEmit
# Watch mode
npx tsc --noEmit --watch
# In CI
npm run typecheck// package.json
{
"scripts": {
"start": "node app.ts",
"typecheck": "tsc --noEmit",
"test": "npm run typecheck && node --test"
}
}