Skip to content
All essays
ArchitectureMarch 27, 202413 min

Fastify Validation and Testing Best Practices

Master validation and testing strategies for robust Fastify applications.

Ü
Ümit Uz
Mobile & Full Stack Developer

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!

Enjoyed this essay? Share it.

Related essays

Next essay
Introduction to Fastify: High-Performance Node.js Framework