Build robust Fastify applications with comprehensive validation and testing strategies.
JSON Schema Validation
Request Validation
javascript
fastify.post('/register', {
schema: {
body: {
type: 'object',
required: ['email', 'password', 'name'],
properties: {
email: {
type: 'string',
format: 'email',
errorMessage: 'Invalid email format',
},
password: {
type: 'string',
minLength: 8,
pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)',
errorMessage: 'Password must be at least 8 characters with uppercase, lowercase, and number',
},
name: {
type: 'string',
minLength: 2,
maxLength: 50,
},
age: {
type: 'number',
minimum: 18,
maximum: 120,
},
},
},
},
}, async (request, reply) => {
const user = await createUser(request.body);
return reply.code(201).send(user);
});Response Validation
javascript
fastify.get('/users/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' },
},
},
404: {
type: 'object',
properties: {
error: { type: 'string' },
},
},
},
},
}, async (request, reply) => {
const user = await findUser(request.params.id);
if (!user) {
return reply.code(404).send({ error: 'User not found' });
}
return user;
});Advanced Validation
Shared Schemas
javascript
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
};
fastify.addSchema({
$id: 'user',
...userSchema,
});
fastify.get('/user/:id', {
schema: {
response: {
200: {
type: 'object',
properties: {
user: { $ref: 'user#' },
},
},
},
},
}, async (request, reply) => {
return { user: await findUser(request.params.id) };
});Custom Validators
javascript
const fastify = require('fastify')();
fastify.post('/data', {
schema: {
body: {
type: 'object',
properties: {
username: {
type: 'string',
// Custom validation
pattern: '^[a-zA-Z0-9_]{3,20}$',
},
},
},
},
validatorCompiler: ({ schema, method, url, httpPart }) => {
return data => {
// Custom validation logic
if (httpPart === 'body' && data.username) {
if (!/^[a-zA-Z0-9_]{3,20}$/.test(data.username)) {
return {
error: new Error('Username must be 3-20 alphanumeric characters'),
};
}
}
return { value: data };
};
},
}, async (request, reply) => {
return { success: true };
});Validation Errors
Custom Error Messages
javascript
fastify.setValidatorCompiler(function ({ schema, method, url, httpPart }) {
return ajv.compile(schema);
});
fastify.setErrorHandler((error, request, reply) => {
if (error.validation) {
const errors = error.validation.map(err => ({
field: err.instancePath.replace('/', ''),
message: err.message,
}));
reply.status(400).send({
success: false,
error: 'Validation Failed',
details: errors,
});
return;
}
reply.status(error.statusCode || 500).send({
success: false,
error: error.message,
});
});Testing with Jest
Setup
javascript
// app.js
const fastify = require('fastify')();
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
module.exports = fastify;
// test/app.test.js
const build = require('../app');
test('GET / route', async () => {
const app = build();
const response = await app.inject({
method: 'GET',
url: '/',
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ hello: 'world' });
await app.close();
});Testing Routes
javascript
describe('User Routes', () => {
let app;
beforeEach(() => {
app = build();
});
afterEach(async () => {
await app.close();
});
test('POST /users creates user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'SecurePass123',
};
const response = await app.inject({
method: 'POST',
url: '/users',
payload: userData,
});
expect(response.statusCode).toBe(201);
expect(response.json()).toHaveProperty('id');
expect(response.json().name).toBe(userData.name);
});
test('POST /users validates input', async () => {
const response = await app.inject({
method: 'POST',
url: '/users',
payload: {
name: 'Jo', // Too short
email: 'invalid-email',
},
});
expect(response.statusCode).toBe(400);
expect(response.json()).toHaveProperty('details');
});
});Testing Authentication
javascript
describe('Protected Routes', () => {
test('GET /protected requires auth', async () => {
const app = build();
const response = await app.inject({
method: 'GET',
url: '/protected',
});
expect(response.statusCode).toBe(401);
});
test('GET /protected accepts valid token', async () => {
const app = build();
const token = generateTestToken();
const response = await app.inject({
method: 'GET',
url: '/protected',
headers: {
authorization: `Bearer ${token}`,
},
});
expect(response.statusCode).toBe(200);
});
});Testing with Supertest
Setup
javascript
const request = require('supertest');
const app = require('./app');
describe('API Tests', () => {
test('GET /users returns users', async () => {
const response = await request(app.server)
.get('/users')
.expect(200);
expect(response.body).toHaveProperty('users');
});
});Integration Testing
Database Tests
javascript
describe('User Integration Tests', () => {
let app;
let db;
beforeAll(async () => {
app = build({ test: true });
db = app.db;
await app.ready();
});
afterAll(async () => {
await db.close();
await app.close();
});
beforeEach(async () => {
await db.users.deleteMany({});
});
test('creates and retrieves user', async () => {
// Create user
const createResponse = await app.inject({
method: 'POST',
url: '/users',
payload: {
name: 'Test User',
email: 'test@example.com',
},
});
expect(createResponse.statusCode).toBe(201);
// Retrieve user
const user = await db.users.findOne({
email: 'test@example.com',
});
expect(user).toBeTruthy();
expect(user.name).toBe('Test User');
});
});Load Testing
With Autocannon
javascript
const autocannon = require('autocannon');
const app = require('./app');
const run = async () => {
await app.ready();
const result = await autocannon({
url: 'http://localhost:3000/api/users',
connections: 100,
duration: 10,
});
console.log(result);
await app.close();
};
run();Best Practices
DOs
- Validate all inputs
- Use JSON Schema
- Test all routes
- Test validation
- Mock external dependencies
- Clean up in tests
- Test error scenarios
DON'Ts
- Skip validation
- Ignore error cases
- Test only happy paths
- Create tightly coupled tests
- Forget about cleanup
- Skip integration tests
- Ignore performance
Conclusion
Validation and testing are crucial for robust Fastify applications. Use JSON Schema for validation, write comprehensive tests, and test all scenarios. Your future self will thank you!