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 close-with-grace for proper shutdown handling:
import Fastify from 'fastify';
import closeWithGrace from 'close-with-grace';
const app = Fastify({ logger: true });
// Register plugins and routes
await app.register(import('./plugins/index.js'));
await app.register(import('./routes/index.js'));
// Graceful shutdown handler
closeWithGrace({ delay: 10000 }, async ({ signal, err }) => {
if (err) {
app.log.error({ err }, 'Server closing due to error');
} else {
app.log.info({ signal }, 'Server closing due to signal');
}
await app.close();
});
// Start server
await app.listen({
port: parseInt(process.env.PORT || '3000', 10),
host: '0.0.0.0',
});
app.log.info(`Server listening on ${app.server.address()}`);Implement comprehensive health checks:
app.get('/health', async () => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
app.get('/health/live', async () => {
return { status: 'ok' };
});
app.get('/health/ready', async (request, reply) => {
const checks = {
database: false,
cache: false,
};
try {
await app.db`SELECT 1`;
checks.database = true;
} catch {
// Database not ready
}
try {
await app.cache.ping();
checks.cache = true;
} catch {
// Cache not ready
}
const allHealthy = Object.values(checks).every(Boolean);
if (!allHealthy) {
reply.code(503);
}
return {
status: allHealthy ? 'ok' : 'degraded',
checks,
timestamp: new Date().toISOString(),
};
});
// Detailed health for monitoring
app.get('/health/details', {
preHandler: [app.authenticate, app.requireAdmin],
}, async () => {
const memory = process.memoryUsage();
return {
status: 'ok',
uptime: process.uptime(),
memory: {
heapUsed: Math.round(memory.heapUsed / 1024 / 1024),
heapTotal: Math.round(memory.heapTotal / 1024 / 1024),
rss: Math.round(memory.rss / 1024 / 1024),
},
version: process.env.APP_VERSION,
nodeVersion: process.version,
};
});Create an optimized Dockerfile:
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Production stage
FROM node:22-alpine
WORKDIR /app
# Run as non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Copy from builder
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/src ./src
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
USER nodejs
EXPOSE 3000
ENV NODE_ENV=production
ENV PORT=3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "src/app.ts"]# docker-compose.yml
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:pass@db:5432/app
- JWT_SECRET=${JWT_SECRET}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:Deploy to Kubernetes:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastify-api
spec:
replicas: 3
selector:
matchLabels:
app: fastify-api
template:
metadata:
labels:
app: fastify-api
spec:
containers:
- name: api
image: my-registry/fastify-api:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
---
apiVersion: v1
kind: Service
metadata:
name: fastify-api
spec:
selector:
app: fastify-api
ports:
- port: 80
targetPort: 3000
type: ClusterIPConfigure logging for production:
import Fastify from 'fastify';
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL || 'info',
// JSON output for log aggregation
formatters: {
level: (label) => ({ level: label }),
bindings: (bindings) => ({
pid: bindings.pid,
hostname: bindings.hostname,
service: 'fastify-api',
version: process.env.APP_VERSION,
}),
},
timestamp: () => `,"time":"${new Date().toISOString()}"`,
// Redact sensitive data
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'*.password',
'*.token',
'*.secret',
],
censor: '[REDACTED]',
},
},
});Configure appropriate timeouts:
const app = Fastify({
connectionTimeout: 30000, // 30s connection timeout
keepAliveTimeout: 72000, // 72s keep-alive (longer than ALB 60s)
requestTimeout: 30000, // 30s request timeout
bodyLimit: 1048576, // 1MB body limit
});
// Per-route timeout
app.get('/long-operation', {
config: {
timeout: 60000, // 60s for this route
},
}, longOperationHandler);Configure for load balancers:
const app = Fastify({
// Trust first proxy (load balancer)
trustProxy: true,
// Or trust specific proxies
trustProxy: ['127.0.0.1', '10.0.0.0/8'],
// Or number of proxies to trust
trustProxy: 1,
});
// Now request.ip returns real client IPServe static files efficiently. Always use import.meta.dirname as the base path, never process.cwd():
import fastifyStatic from '@fastify/static';
import { join } from 'node:path';
app.register(fastifyStatic, {
root: join(import.meta.dirname, '..', 'public'),
prefix: '/static/',
maxAge: '1d',
immutable: true,
etag: true,
lastModified: true,
});Enable response compression:
import fastifyCompress from '@fastify/compress';
app.register(fastifyCompress, {
global: true,
threshold: 1024, // Only compress > 1KB
encodings: ['gzip', 'deflate'],
});Expose Prometheus metrics:
import { register, collectDefaultMetrics, Counter, Histogram } from 'prom-client';
collectDefaultMetrics();
const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
});
const httpRequestTotal = new Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status'],
});
app.addHook('onResponse', (request, reply, done) => {
const route = request.routeOptions.url || request.url;
const labels = {
method: request.method,
route,
status: reply.statusCode,
};
httpRequestDuration.observe(labels, reply.elapsedTime / 1000);
httpRequestTotal.inc(labels);
done();
});
app.get('/metrics', async (request, reply) => {
reply.header('Content-Type', register.contentType);
return register.metrics();
});Support rolling updates:
import closeWithGrace from 'close-with-grace';
// Stop accepting new connections gracefully
closeWithGrace({ delay: 30000 }, async ({ signal }) => {
app.log.info({ signal }, 'Received shutdown signal');
// Stop accepting new connections
// Existing connections continue to be served
// Wait for in-flight requests (handled by close-with-grace delay)
await app.close();
app.log.info('Server closed');
});