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
Add WebSocket support to Fastify:
import Fastify from 'fastify';
import websocket from '@fastify/websocket';
const app = Fastify();
app.register(websocket);
app.get('/ws', { websocket: true }, (socket, request) => {
socket.on('message', (message) => {
const data = message.toString();
console.log('Received:', data);
// Echo back
socket.send(`Echo: ${data}`);
});
socket.on('close', () => {
console.log('Client disconnected');
});
socket.on('error', (error) => {
console.error('WebSocket error:', error);
});
});
await app.listen({ port: 3000 });Use Fastify hooks with WebSocket routes:
app.register(async function wsRoutes(fastify) {
// This hook runs before WebSocket upgrade
fastify.addHook('preValidation', async (request, reply) => {
const token = request.headers.authorization;
if (!token) {
reply.code(401).send({ error: 'Unauthorized' });
return;
}
request.user = await verifyToken(token);
});
fastify.get('/ws', { websocket: true }, (socket, request) => {
console.log('Connected user:', request.user.id);
socket.on('message', (message) => {
// Handle authenticated messages
});
});
});Configure WebSocket server options:
app.register(websocket, {
options: {
maxPayload: 1048576, // 1MB max message size
clientTracking: true,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3,
},
zlibInflateOptions: {
chunkSize: 10 * 1024,
},
},
},
});Broadcast messages to connected clients:
const clients = new Set<WebSocket>();
app.get('/ws', { websocket: true }, (socket, request) => {
clients.add(socket);
socket.on('close', () => {
clients.delete(socket);
});
socket.on('message', (message) => {
// Broadcast to all other clients
for (const client of clients) {
if (client !== socket && client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
});
});
// Broadcast from HTTP route
app.post('/broadcast', async (request) => {
const { message } = request.body;
for (const client of clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'broadcast', message }));
}
}
return { sent: clients.size };
});Organize connections into rooms:
const rooms = new Map<string, Set<WebSocket>>();
function joinRoom(socket: WebSocket, roomId: string) {
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
}
rooms.get(roomId)!.add(socket);
}
function leaveRoom(socket: WebSocket, roomId: string) {
rooms.get(roomId)?.delete(socket);
if (rooms.get(roomId)?.size === 0) {
rooms.delete(roomId);
}
}
function broadcastToRoom(roomId: string, message: string, exclude?: WebSocket) {
const room = rooms.get(roomId);
if (!room) return;
for (const client of room) {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(message);
}
}
}
app.get('/ws/:roomId', { websocket: true }, (socket, request) => {
const { roomId } = request.params as { roomId: string };
joinRoom(socket, roomId);
socket.on('message', (message) => {
broadcastToRoom(roomId, message.toString(), socket);
});
socket.on('close', () => {
leaveRoom(socket, roomId);
});
});Use JSON for structured messages:
interface WSMessage {
type: string;
payload?: unknown;
id?: string;
}
app.get('/ws', { websocket: true }, (socket, request) => {
function send(message: WSMessage) {
socket.send(JSON.stringify(message));
}
socket.on('message', (raw) => {
let message: WSMessage;
try {
message = JSON.parse(raw.toString());
} catch {
send({ type: 'error', payload: 'Invalid JSON' });
return;
}
switch (message.type) {
case 'ping':
send({ type: 'pong', id: message.id });
break;
case 'subscribe':
handleSubscribe(socket, message.payload);
send({ type: 'subscribed', payload: message.payload, id: message.id });
break;
case 'message':
handleMessage(socket, message.payload);
break;
default:
send({ type: 'error', payload: 'Unknown message type' });
}
});
});Keep connections alive:
const HEARTBEAT_INTERVAL = 30000;
const clients = new Map<WebSocket, { isAlive: boolean }>();
app.get('/ws', { websocket: true }, (socket, request) => {
clients.set(socket, { isAlive: true });
socket.on('pong', () => {
const client = clients.get(socket);
if (client) client.isAlive = true;
});
socket.on('close', () => {
clients.delete(socket);
});
});
// Heartbeat interval
setInterval(() => {
for (const [socket, state] of clients) {
if (!state.isAlive) {
socket.terminate();
clients.delete(socket);
continue;
}
state.isAlive = false;
socket.ping();
}
}, HEARTBEAT_INTERVAL);Authenticate WebSocket connections:
app.get('/ws', {
websocket: true,
preValidation: async (request, reply) => {
// Authenticate via query parameter or header
const token = request.query.token || request.headers.authorization?.replace('Bearer ', '');
if (!token) {
reply.code(401).send({ error: 'Token required' });
return;
}
try {
request.user = await verifyToken(token);
} catch {
reply.code(401).send({ error: 'Invalid token' });
}
},
}, (socket, request) => {
console.log('Authenticated user:', request.user);
socket.on('message', (message) => {
// Handle authenticated messages
});
});Handle WebSocket errors properly:
app.get('/ws', { websocket: true }, (socket, request) => {
socket.on('error', (error) => {
request.log.error({ err: error }, 'WebSocket error');
});
socket.on('message', async (raw) => {
try {
const message = JSON.parse(raw.toString());
const result = await processMessage(message);
socket.send(JSON.stringify({ success: true, result }));
} catch (error) {
request.log.error({ err: error }, 'Message processing error');
socket.send(JSON.stringify({
success: false,
error: error.message,
}));
}
});
});Limit message frequency:
const rateLimits = new Map<WebSocket, { count: number; resetAt: number }>();
function checkRateLimit(socket: WebSocket, limit: number, window: number): boolean {
const now = Date.now();
let state = rateLimits.get(socket);
if (!state || now > state.resetAt) {
state = { count: 0, resetAt: now + window };
rateLimits.set(socket, state);
}
state.count++;
if (state.count > limit) {
return false;
}
return true;
}
app.get('/ws', { websocket: true }, (socket, request) => {
socket.on('message', (message) => {
if (!checkRateLimit(socket, 100, 60000)) {
socket.send(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
// Process message
});
socket.on('close', () => {
rateLimits.delete(socket);
});
});Close WebSocket connections on shutdown:
import closeWithGrace from 'close-with-grace';
const connections = new Set<WebSocket>();
app.get('/ws', { websocket: true }, (socket, request) => {
connections.add(socket);
socket.on('close', () => {
connections.delete(socket);
});
});
closeWithGrace({ delay: 5000 }, async ({ signal }) => {
// Notify clients
for (const socket of connections) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'shutdown', message: 'Server is shutting down' }));
socket.close(1001, 'Server shutdown');
}
}
await app.close();
});Use WebSocket for streaming data:
app.get('/ws/stream', { websocket: true }, async (socket, request) => {
const stream = createDataStream();
stream.on('data', (data) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'data', payload: data }));
}
});
stream.on('end', () => {
socket.send(JSON.stringify({ type: 'end' }));
socket.close();
});
socket.on('message', (message) => {
const { type, payload } = JSON.parse(message.toString());
if (type === 'pause') {
stream.pause();
} else if (type === 'resume') {
stream.resume();
}
});
socket.on('close', () => {
stream.destroy();
});
});