The presentation layer provides a unified route system that powers:
- Type-safe route definitions with schema validation
- Server-side handlers with request/response mapping
- Framework adapters (Hono, Fastify, Elysia, NestJS)
- OpenAPI specification generation
- Type-safe client generation
Structure
presentation/
└── http/
├── routes/
│ ├── users.routes.ts ← Route definitions with schemas
│ └── projects.routes.ts
├── handlers/
│ ├── users.handlers.ts ← Handler implementations
│ └── projects.handlers.ts
└── router.ts ← Combined router definition
Route Definitions
Define routes using defineRoute() with full type safety:
import { defineRoute } from '@cosmneo/onion-lasagna/http/route';
import { zodSchema, z } from '@cosmneo/onion-lasagna/http/schema/zod';
export const createUserRoute = defineRoute({
method: 'POST',
path: '/api/users',
request: {
body: {
schema: zodSchema(
z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
}),
),
},
context: {
schema: zodSchema(
z.object({
userId: z.string(),
}),
),
},
},
responses: {
201: {
description: 'User created',
schema: zodSchema(
z.object({
userId: z.string().uuid(),
}),
),
},
400: { description: 'Validation error' },
},
docs: {
summary: 'Create a new user',
tags: ['Users'],
operationId: 'createUser',
},
});
Route Definition Options
| Property | Description |
|---|---|
method | HTTP method: GET, POST, PUT, PATCH, DELETE |
path | URL path with :param syntax for path parameters |
request.body | Request body schema |
request.query | Query parameter schema |
request.params | Path parameter schema |
request.context | Context schema (from middleware) |
responses | Response schemas by status code |
docs | OpenAPI documentation |
Router Definition
Group routes into a router using defineRouter():
import { defineRouter } from '@cosmneo/onion-lasagna/http/route';
import { createUserRoute, listUsersRoute, getUserRoute } from './routes/users.routes';
export const userRouter = defineRouter({
users: {
create: createUserRoute,
list: listUsersRoute,
get: getUserRoute,
},
});
Routers can be nested for complex APIs:
export const apiRouter = defineRouter({
projects: {
create: createProjectRoute,
list: listProjectsRoute,
get: getProjectRoute,
tasks: {
add: addTaskRoute,
list: listTasksRoute,
update: updateTaskRoute,
},
},
});
Server Routes (Handlers)
Create handlers using the serverRoutes() builder:
import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
import { userRouter } from './router';
export function createUserHandlers(useCases: UseCases) {
return serverRoutes(userRouter)
.handle('users.create', {
requestMapper: (req, ctx) => ({
email: req.body.email,
name: req.body.name,
createdBy: ctx.userId,
}),
useCase: useCases.createUserUseCase,
responseMapper: (output) => ({
status: 201 as const,
body: { userId: output.userId },
}),
})
.handle('users.list', {
requestMapper: (req) => ({
page: req.query?.page ?? 1,
pageSize: req.query?.pageSize ?? 20,
}),
useCase: useCases.listUsersUseCase,
responseMapper: (output) => ({
status: 200 as const,
body: output,
}),
})
.build();
}
Handler Configuration
Each handler has three functions:
| Function | Purpose |
|---|---|
requestMapper | Transform validated HTTP request to use case input |
useCase | The use case to execute |
responseMapper | Transform use case output to HTTP response |
The req parameter in requestMapper is fully typed based on your route definition:
req.body- Validated request bodyreq.query- Validated query parametersreq.pathParams- Validated path parameters
The ctx parameter contains context from middleware (e.g., authenticated user).
Framework Integration
Register routes with your framework of choice:
Hono
import { Hono } from 'hono';
import { registerHonoRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/hono';
const app = new Hono();
app.onError((err, c) => onionErrorHandler(err, c));
registerHonoRoutes(app, routes, {
middlewares: [authMiddleware],
contextExtractor: (c) => ({
userId: c.get('jwtPayload')?.sub,
}),
});
Fastify
import Fastify from 'fastify';
import { registerFastifyRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/fastify';
const app = Fastify();
app.setErrorHandler(onionErrorHandler);
registerFastifyRoutes(app, routes, {
middlewares: [authMiddleware],
contextExtractor: (request) => ({
userId: request.user?.userId,
}),
});
Elysia
import { Elysia } from 'elysia';
import { registerElysiaRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/elysia';
const app = new Elysia()
.onError(({ error }) => onionErrorHandler({ error }));
registerElysiaRoutes(app, routes, {
middlewares: [authMiddleware],
contextExtractor: (ctx) => ({
userId: ctx.store['userId'],
}),
});
NestJS
NestJS uses a decorator-based approach:
import { Controller, Post, UseGuards, UseFilters } from '@nestjs/common';
import { OnionRequest, OnionExceptionFilter } from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
import type { ContextualRawHttpRequest } from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
@Controller('api/users')
@UseGuards(JwtAuthGuard)
@UseFilters(OnionExceptionFilter)
export class UsersController {
constructor(private readonly routeHandlers: Map<string, UnifiedRouteInput>) {}
@Post()
async create(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
) {
const route = this.routeHandlers.get('POST:/api/users');
return route.handler(request, request.context);
}
}
Context Extraction
Context extractors provide request context (e.g., authenticated user) to handlers:
// Define context type
interface AuthContext {
userId: string;
roles: string[];
}
// Create extractor
const contextExtractor = (c: Context): AuthContext => ({
userId: c.get('jwtPayload')?.sub ?? '',
roles: c.get('jwtPayload')?.roles ?? [],
});
// Use in handler
.handle('projects.create', {
requestMapper: (req, ctx) => ({
name: req.body.name,
createdBy: ctx.userId, // From context
}),
// ...
})
Error Handling
The unified route system provides automatic error handling:
| Error Type | HTTP Status | Masked |
|---|---|---|
ObjectValidationError | 400 | No |
InvalidRequestError | 400 | No |
UseCaseError | 400 | No |
AccessDeniedError | 403 | No |
NotFoundError | 404 | No |
ConflictError | 409 | No |
UnprocessableError | 422 | No |
DomainError | 500 | Yes |
InfraError | 500 | Yes |
ControllerError | 500 | Yes |
500-level errors are masked for security - only a generic message is returned to clients.
Use the framework-specific error handler:
// Hono
app.onError(onionErrorHandler);
// Fastify
app.setErrorHandler(onionErrorHandler);
// Elysia
app.onError(onionErrorHandler);
// NestJS
@UseFilters(OnionExceptionFilter)
OpenAPI Generation
Generate OpenAPI specifications from your router:
import { generateOpenAPI } from '@cosmneo/onion-lasagna/http/openapi';
const spec = generateOpenAPI(userRouter, {
info: {
title: 'User API',
version: '1.0.0',
description: 'API for user management',
},
servers: [
{ url: 'https://api.example.com', description: 'Production' },
{ url: 'http://localhost:3000', description: 'Development' },
],
tags: [
{ name: 'Users', description: 'User operations' },
],
});
Type-Safe Client
Generate a type-safe client from your router:
import { createClient } from '@cosmneo/onion-lasagna/http/client';
const client = createClient(userRouter, {
baseUrl: 'https://api.example.com',
headers: { Authorization: `Bearer ${token}` },
});
// Fully typed API calls
const user = await client.users.get({
pathParams: { userId: '123' },
});
const newUser = await client.users.create({
body: { email: 'john@example.com', name: 'John' },
});
Partial Builds
For large routers, use buildPartial() to build handlers incrementally:
// users.handlers.ts
export function createUserHandlers(useCases: UseCases) {
return serverRoutes(apiRouter)
.handle('users.create', { ... })
.handle('users.list', { ... })
.buildPartial(); // Returns partial routes
}
// projects.handlers.ts
export function createProjectHandlers(useCases: UseCases) {
return serverRoutes(apiRouter)
.handle('projects.create', { ... })
.handle('projects.list', { ... })
.buildPartial();
}
// index.ts - Combine all handlers
const routes = [
...createUserHandlers(useCases),
...createProjectHandlers(useCases),
];