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 Type | HTTP Status | Body |
|---|---|---|
ObjectValidationError | 400 | { errorCode, message, details } |
InvalidRequestError | 400 | { errorCode, message, details } |
UseCaseError | 400 | { errorCode, message } |
AccessDeniedError | 403 | { errorCode, message } |
NotFoundError | 404 | { errorCode, message } |
ConflictError | 409 | { errorCode, message } |
UnprocessableError | 422 | { errorCode, message } |
DomainError | 500 | Masked |
InfraError | 500 | Masked |
| Unknown | 500 | Masked |
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);
}