Fastify best practices skill
93
97%
Does it follow best practices?
Impact
85%
1.37xAverage score across 4 eval scenarios
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;
});