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
Fastify includes parsers for common content types:
import Fastify from 'fastify';
const app = Fastify();
// Built-in parsers:
// - application/json
// - text/plain
app.post('/json', async (request) => {
// request.body is parsed JSON object
return { received: request.body };
});
app.post('/text', async (request) => {
// request.body is string for text/plain
return { text: request.body };
});Add parsers for additional content types:
// Parse application/x-www-form-urlencoded
app.addContentTypeParser(
'application/x-www-form-urlencoded',
{ parseAs: 'string' },
(request, body, done) => {
const parsed = new URLSearchParams(body);
done(null, Object.fromEntries(parsed));
},
);
// Async parser
app.addContentTypeParser(
'application/x-www-form-urlencoded',
{ parseAs: 'string' },
async (request, body) => {
const parsed = new URLSearchParams(body);
return Object.fromEntries(parsed);
},
);Parse XML content:
import { XMLParser } from 'fast-xml-parser';
const xmlParser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
});
app.addContentTypeParser(
'application/xml',
{ parseAs: 'string' },
async (request, body) => {
return xmlParser.parse(body);
},
);
app.addContentTypeParser(
'text/xml',
{ parseAs: 'string' },
async (request, body) => {
return xmlParser.parse(body);
},
);
app.post('/xml', async (request) => {
// request.body is parsed XML as JavaScript object
return { data: request.body };
});Use @fastify/multipart for file uploads. Configure these critical options:
import fastifyMultipart from '@fastify/multipart';
app.register(fastifyMultipart, {
// CRITICAL: Always set explicit limits
limits: {
fieldNameSize: 100, // Max field name size in bytes
fieldSize: 1024 * 1024, // Max field value size (1MB)
fields: 10, // Max number of non-file fields
fileSize: 10 * 1024 * 1024, // Max file size (10MB)
files: 5, // Max number of files
headerPairs: 2000, // Max number of header pairs
parts: 1000, // Max number of parts (fields + files)
},
// IMPORTANT: Throw on limit exceeded (default is to truncate silently!)
throwFileSizeLimit: true,
// Attach all fields to request.body for easier access
attachFieldsToBody: true,
// Only accept specific file types (security!)
// onFile: async (part) => {
// if (!['image/jpeg', 'image/png'].includes(part.mimetype)) {
// throw new Error('Invalid file type');
// }
// },
});
// Handle file upload
app.post('/upload', async (request, reply) => {
const data = await request.file();
if (!data) {
return reply.code(400).send({ error: 'No file uploaded' });
}
// data.file is a stream
const buffer = await data.toBuffer();
return {
filename: data.filename,
mimetype: data.mimetype,
size: buffer.length,
};
});
// Handle multiple files
app.post('/upload-multiple', async (request) => {
const files = [];
for await (const part of request.files()) {
const buffer = await part.toBuffer();
files.push({
filename: part.filename,
mimetype: part.mimetype,
size: buffer.length,
});
}
return { files };
});
// Handle mixed form data
app.post('/form', async (request) => {
const parts = request.parts();
const fields: Record<string, string> = {};
const files: Array<{ name: string; size: number }> = [];
for await (const part of parts) {
if (part.type === 'file') {
const buffer = await part.toBuffer();
files.push({ name: part.filename, size: buffer.length });
} else {
fields[part.fieldname] = part.value as string;
}
}
return { fields, files };
});Process body as stream for large payloads:
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';
// Add parser that returns stream
app.addContentTypeParser(
'application/octet-stream',
async (request, payload) => {
return payload; // Return stream directly
},
);
app.post('/upload-stream', async (request, reply) => {
const destination = createWriteStream('./upload.bin');
await pipeline(request.body, destination);
return { success: true };
});Replace the default JSON parser:
// Remove default parser
app.removeContentTypeParser('application/json');
// Add custom parser with error handling
app.addContentTypeParser(
'application/json',
{ parseAs: 'string' },
async (request, body) => {
try {
return JSON.parse(body);
} catch (error) {
throw {
statusCode: 400,
code: 'INVALID_JSON',
message: 'Invalid JSON payload',
};
}
},
);Handle content types with parameters:
// Match content type with any charset
app.addContentTypeParser(
'application/json; charset=utf-8',
{ parseAs: 'string' },
async (request, body) => {
return JSON.parse(body);
},
);
// Use regex for flexible matching
app.addContentTypeParser(
/^application\/.*\+json$/,
{ parseAs: 'string' },
async (request, body) => {
return JSON.parse(body);
},
);Handle unknown content types:
app.addContentTypeParser('*', async (request, payload) => {
const chunks: Buffer[] = [];
for await (const chunk of payload) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
// Try to determine content type
const contentType = request.headers['content-type'];
if (contentType?.includes('json')) {
return JSON.parse(buffer.toString('utf-8'));
}
if (contentType?.includes('text')) {
return buffer.toString('utf-8');
}
return buffer;
});Configure body size limits:
// Global limit
const app = Fastify({
bodyLimit: 1048576, // 1MB
});
// Per-route limit
app.post('/large-upload', {
bodyLimit: 52428800, // 50MB for this route
}, async (request) => {
return { size: JSON.stringify(request.body).length };
});
// Per content type limit
app.addContentTypeParser('application/json', {
parseAs: 'string',
bodyLimit: 2097152, // 2MB for JSON
}, async (request, body) => {
return JSON.parse(body);
});Parse protobuf content:
import protobuf from 'protobufjs';
const root = await protobuf.load('./schema.proto');
const MessageType = root.lookupType('package.MessageType');
app.addContentTypeParser(
'application/x-protobuf',
{ parseAs: 'buffer' },
async (request, body) => {
const message = MessageType.decode(body);
return MessageType.toObject(message);
},
);Simple form parsing:
import formbody from '@fastify/formbody';
app.register(formbody);
app.post('/form', async (request) => {
// request.body is parsed form data
const { name, email } = request.body as { name: string; email: string };
return { name, email };
});Handle different request formats:
app.post('/data', async (request, reply) => {
const contentType = request.headers['content-type'];
// Body is already parsed by the appropriate parser
const data = request.body;
// Respond based on Accept header
const accept = request.headers.accept;
if (accept?.includes('application/xml')) {
reply.type('application/xml');
return `<data>${JSON.stringify(data)}</data>`;
}
reply.type('application/json');
return data;
});Validate parsed content:
app.post('/users', {
schema: {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
},
required: ['name', 'email'],
},
},
}, async (request) => {
// Body is parsed AND validated
return request.body;
});