Fastify Integration

Register routes and handle errors with Fastify framework.

Installation

bun add fastify @fastify/cors @fastify/jwt

Import the Fastify integration:

import {
  registerFastifyRoutes,
  onionErrorHandler,
} from '@cosmneo/onion-lasagna/http/frameworks/fastify';

Quick Start

import Fastify from 'fastify';
import { registerFastifyRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/fastify';
import { bootstrapProjectManagement } from './bootstrap';

const { routes } = bootstrapProjectManagement();

const app = Fastify();

// Apply error handler
app.setErrorHandler(onionErrorHandler);

// Register routes
registerFastifyRoutes(app, routes);

app.listen({ port: 3000 });

Registering Routes

The registerFastifyRoutes function registers routes with Fastify:

import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
import { registerFastifyRoutes } from '@cosmneo/onion-lasagna/http/frameworks/fastify';

// Create routes using the builder
const routes = serverRoutes(userRouter)
  .handle('users.get', {
    requestMapper: (req) => ({ userId: req.pathParams.userId }),
    useCase: getUserQuery,
    responseMapper: (output) => ({ status: 200 as const, body: output }),
  })
  .build();

// Register with Fastify
registerFastifyRoutes(app, routes);

Options

interface RegisterFastifyRoutesOptions {
  prefix?: string;                      // Route prefix (e.g., '/api')
  preHandlers?: FastifyMiddleware[];    // Fastify preHandlers (preferred)
  middlewares?: FastifyMiddleware[];    // Alias for preHandlers
  contextExtractor?: FastifyContextExtractor;  // Extract auth context
}

registerFastifyRoutes(app, routes, {
  prefix: '/api',
  preHandlers: [authMiddleware],
  contextExtractor: (request) => ({
    userId: request.user?.userId,
  }),
});

Context Extraction (Protected Routes)

For authenticated routes, provide a contextExtractor to pass auth data to handlers:

import fastifyJwt from '@fastify/jwt';

// Register JWT plugin
await app.register(fastifyJwt, {
  secret: process.env.JWT_SECRET,
});

// Auth preHandler
const authMiddleware = async (request: FastifyRequest, reply: FastifyReply) => {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.status(401).send({ message: 'Unauthorized' });
  }
};

// Register with context extraction
registerFastifyRoutes(app, routes, {
  preHandlers: [authMiddleware],
  contextExtractor: (request) => ({
    userId: request.user?.sub,
  }),
});

Error Handler

The onionErrorHandler maps domain errors to HTTP responses:

app.setErrorHandler(onionErrorHandler);

Error Mapping

Error TypeHTTP StatusBody
ObjectValidationError400{ errorCode, message, details }
InvalidRequestError400{ errorCode, message, details }
UseCaseError400{ errorCode, message }
AccessDeniedError403{ errorCode, message }
NotFoundError404{ errorCode, message }
ConflictError409{ errorCode, message }
UnprocessableError422{ errorCode, message }
DomainError500Masked
InfraError500Masked
Unknown500Masked

Custom Error Handling

Log errors before handling:

app.setErrorHandler((error, request, reply) => {
  console.error('Error:', error);
  return onionErrorHandler(error, request, reply);
});

Complete Example

import 'dotenv/config';
import Fastify from 'fastify';
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
import { registerFastifyRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/fastify';
import { bootstrapProjectManagement } from './bootstrap';

const { routes } = bootstrapProjectManagement();

const app = Fastify({ logger: true });

// Plugins
await app.register(cors);
await app.register(jwt, {
  secret: process.env.JWT_SECRET ?? 'dev-secret',
});

// Error handling
app.setErrorHandler((error, request, reply) => {
  console.error('Error:', error);
  return onionErrorHandler(error, request, reply);
});

// Health check (no auth)
app.get('/health', () => ({ status: 'ok' }));

// Auth preHandler
const authMiddleware = async (request, reply) => {
  try {
    await request.jwtVerify();
  } catch (err) {
    reply.status(401).send({ message: 'Unauthorized' });
  }
};

// API routes with auth
registerFastifyRoutes(app, routes, {
  preHandlers: [authMiddleware],
  contextExtractor: (request) => ({
    userId: request.user?.sub,
  }),
});

// Start server
try {
  await app.listen({ port: 3000 });
  console.log('Server running on http://localhost:3000');
} catch (err) {
  app.log.error(err);
  process.exit(1);
}