Overview
In onion-lasagna, DTOs are plain TypeScript interfaces rather than classes. Validation is handled at the HTTP boundary through schema adapters, and data transformation happens in request/response mappers.
DTO Types
| DTO Type | Purpose | Where Defined |
|---|---|---|
| Request Body | HTTP request body shape | Route definition (request.body.schema) |
| Query Params | HTTP query parameters | Route definition (request.query.schema) |
| Path Params | URL path parameters | Route definition (request.params.schema) |
| Response Body | HTTP response shape | Route definition (responses.{status}.schema) |
| Use Case Input | Input to use case | TypeScript interface in use case port |
| Use Case Output | Output from use case | TypeScript interface in use case port |
Schema-Based Validation
Validation is defined in route definitions using schema adapters:
import { defineRoute } from '@cosmneo/onion-lasagna/http/route';
import { zodSchema, z } from '@cosmneo/onion-lasagna/http/schema/zod';
// Define the route with schemas
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),
password: z.string().min(8),
}),
),
},
},
responses: {
201: {
description: 'User created',
schema: zodSchema(
z.object({
userId: z.string().uuid(),
}),
),
},
},
});
The schema adapter validates incoming requests automatically before your handler runs.
Use Case DTOs
Use case input and output are plain TypeScript interfaces:
// Input DTO - what the use case receives
interface CreateUserInput {
email: string;
name: string;
hashedPassword: string;
}
// Output DTO - what the use case returns
interface CreateUserOutput {
userId: string;
email: string;
name: string;
createdAt: Date;
}
// Use case port definition
interface CreateUserUseCasePort {
execute(input: CreateUserInput): Promise<CreateUserOutput>;
}
Request and Response Mapping
Transform between HTTP layer and use case layer using mappers:
import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
import { userRouter } from './router';
const routes = serverRoutes(userRouter)
.handle('users.create', {
// Transform validated HTTP request to use case input
requestMapper: (req, ctx) => ({
email: req.body.email,
name: req.body.name,
hashedPassword: hashPassword(req.body.password),
}),
useCase: createUserUseCase,
// Transform use case output to HTTP response
responseMapper: (output) => ({
status: 201 as const,
body: {
userId: output.userId,
},
}),
})
.build();
The req parameter is fully typed based on your route definition schemas.
Schema Adapters
Two schema adapters are available:
Zod Adapter
import { zodSchema, z } from '@cosmneo/onion-lasagna/http/schema/zod';
const userSchema = zodSchema(
z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
}),
);
TypeBox Adapter
import { typeboxSchema, Type } from '@cosmneo/onion-lasagna/http/schema/typebox';
const userSchema = typeboxSchema(
Type.Object({
id: Type.String({ format: 'uuid' }),
email: Type.String({ format: 'email' }),
name: Type.String({ minLength: 1, maxLength: 100 }),
}),
);
Validation Errors
When schema validation fails, an ObjectValidationError is thrown automatically. The framework converts this to a 400 HTTP response with structured errors:
{
"errorCode": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "path": ["body", "email"], "message": "Invalid email" },
{ "path": ["body", "password"], "message": "String must contain at least 8 character(s)" }
]
}
Context DTOs
For authenticated routes, define a context schema to validate JWT payload or middleware data:
// Auth context schema
export const authContextSchema = zodSchema(
z.object({
userId: z.string(),
}),
);
export const createProjectRoute = defineRoute({
method: 'POST',
path: '/api/projects',
request: {
body: {
schema: zodSchema(
z.object({
name: z.string().min(1),
}),
),
},
context: {
schema: authContextSchema,
},
},
// ...
});
Access context in your handler:
.handle('projects.create', {
requestMapper: (req, ctx) => ({
name: req.body.name,
createdBy: ctx.userId, // Fully typed from context schema
}),
// ...
})
Type Inference
Schema adapters provide full type inference:
import type {
InferRouteBody,
InferRouteQuery,
InferRoutePathParams,
InferRouteSuccessResponse,
InferRouteResponse,
} from '@cosmneo/onion-lasagna/http';
// Infer request types
type CreateUserBody = InferRouteBody<typeof createUserRoute>;
// { email: string; name: string; password: string }
// Infer the first 2xx response type
type CreateUserResponse = InferRouteSuccessResponse<typeof createUserRoute>;
// { userId: string }
// Infer response for a specific status code
type NotFoundResponse = InferRouteResponse<typeof getUserRoute, 404>;
// { message: string }
DTO vs Value Object
| Aspect | DTO | Value Object |
|---|---|---|
| Purpose | Transfer data across boundaries | Represent domain concept |
| Location | Presentation / App layer | Domain layer |
| Form | Plain interface + schema | Class with behavior |
| Validation | Schema adapter at boundary | create() factory method |
| Identity | None | Compared by value |