Overview
Schema adapters wrap validation libraries (Zod, TypeBox) with a consistent interface for:
- Request/response validation in route definitions
- JSON Schema generation for OpenAPI documentation
- Full TypeScript type inference
Available adapters:
- Zod - TypeScript-first schema validation (
zodSchema) - TypeBox - JSON Schema Type Builder (
typeboxSchema)
Schema Adapter Interface
All adapters implement SchemaAdapter<TOutput, TInput>:
interface SchemaAdapter<TOutput, TInput = TOutput> {
validate(data: unknown): ValidationResult<TOutput>;
toJsonSchema(options?: JsonSchemaOptions): JsonSchema;
_output: TOutput; // Phantom type for inference
_input: TInput; // Phantom type for inference
}
type ValidationResult<T> =
| { success: true; data: T }
| { success: false; issues: ValidationIssue[] };
Zod Adapter
The Zod adapter wraps Zod schemas for use in route definitions:
import { zodSchema, z } from '@cosmneo/onion-lasagna/http/schema/zod';
// Create a schema adapter
const userSchema = zodSchema(
z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).optional(),
}),
);
// Use in route definition
import { defineRoute } from '@cosmneo/onion-lasagna/http/route';
export const getUserRoute = defineRoute({
method: 'GET',
path: '/api/users/:userId',
responses: {
200: {
description: 'User found',
schema: userSchema,
},
},
});
TypeBox Adapter
TypeBox schemas ARE JSON Schema, making them ideal for OpenAPI generation:
import { typeboxSchema, Type } from '@cosmneo/onion-lasagna/http/schema/typebox';
// Create a schema adapter
const userSchema = typeboxSchema(
Type.Object({
id: Type.String({ format: 'uuid' }),
email: Type.String({ format: 'email' }),
name: Type.String({ minLength: 1, maxLength: 100 }),
age: Type.Optional(Type.Integer({ minimum: 0 })),
}),
);
// Use in route definition
export const getUserRoute = defineRoute({
method: 'GET',
path: '/api/users/:userId',
responses: {
200: {
description: 'User found',
schema: userSchema,
},
},
});
Using in Route Definitions
Schema adapters are used in route definitions for request and response validation:
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: {
// Request body validation
body: {
schema: zodSchema(
z.object({
email: z.string().email(),
name: z.string().min(1),
password: z.string().min(8),
}),
),
},
// Query parameter validation
query: {
schema: zodSchema(
z.object({
sendWelcomeEmail: z.coerce.boolean().optional(),
}),
),
},
// Path parameter validation
params: {
schema: zodSchema(
z.object({
orgId: z.string().uuid(),
}),
),
},
// Context validation (from middleware)
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',
},
},
});
Automatic Validation
When you register routes with a framework adapter, request validation happens automatically:
import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
const routes = serverRoutes(userRouter)
.handle('users.create', {
// req.body, req.query, req.pathParams are already validated and typed
requestMapper: (req, ctx) => ({
email: req.body.email, // string (validated)
name: req.body.name, // string (validated)
createdBy: ctx.userId, // string (from context schema)
}),
useCase: createUserUseCase,
responseMapper: (output) => ({
status: 201 as const,
body: { userId: output.userId },
}),
})
.build();
If validation fails, a 400 response is returned with structured errors:
{
"errorCode": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "path": ["body", "email"], "message": "Invalid email" }
]
}
JSON Schema Generation
Schema adapters can generate JSON Schema for OpenAPI documentation:
const userSchema = zodSchema(
z.object({
name: z.string().describe('User full name'),
email: z.string().email().describe('User email address'),
}),
);
// Get JSON Schema
const jsonSchema = userSchema.toJsonSchema();
// {
// type: 'object',
// properties: {
// name: { type: 'string', description: 'User full name' },
// email: { type: 'string', format: 'email', description: 'User email address' },
// },
// required: ['name', 'email'],
// }
Use generateOpenAPI() to create a full OpenAPI spec from your router:
import { generateOpenAPI } from '@cosmneo/onion-lasagna/http/openapi';
const spec = generateOpenAPI(userRouter, {
info: { title: 'User API', version: '1.0.0' },
servers: [{ url: 'https://api.example.com' }],
});
Type Inference
Schema adapters provide full type inference:
import type { InferOutput, InferInput } from '@cosmneo/onion-lasagna/http';
const userSchema = zodSchema(
z.object({
name: z.string(),
age: z.number(),
}),
);
// Infer types from schema
type User = InferOutput<typeof userSchema>;
// { name: string; age: number }
Types are automatically inferred in handlers:
.handle('users.create', {
requestMapper: (req, ctx) => {
// req.body is typed based on the route's body schema
const email: string = req.body.email; // TypeScript knows this is string
return { email, createdBy: ctx.userId };
},
// ...
})
Value Objects vs Schema Adapters
| Aspect | Schema Adapters | Value Objects |
|---|---|---|
| Purpose | Boundary validation | Domain invariants |
| Location | Route definitions (presentation) | Domain layer |
| Validation | Schema-based (JSON Schema compatible) | Plain TypeScript |
| When to use | HTTP request/response validation | Domain concepts |
Schema adapters validate untrusted input at system boundaries:
// Route definition with schema validation
const createUserRoute = defineRoute({
request: {
body: { schema: zodSchema(z.object({ email: z.string().email() })) },
},
});
Value Objects enforce domain invariants:
// Domain validation with business rules
class Email extends BaseValueObject<string> {
static create(value: string): Email {
if (!Email.isValidEmail(value)) {
throw new InvariantViolationError({ message: 'Invalid email' });
}
return new Email(value);
}
}
Adapter Comparison
| Feature | Zod | TypeBox |
|---|---|---|
| Bundle size | ~12kb | ~15kb |
| Type inference | Excellent | Good |
| Error messages | Detailed, customizable | Basic |
| JSON Schema | Conversion required | Native (zero-cost) |
| Best for | General use | OpenAPI-first APIs |
Choose Zod for:
- Better error messages
- More validation features (refinements, transforms)
- Familiar API
Choose TypeBox for:
- Native JSON Schema (faster OpenAPI generation)
- Better performance
- Smaller runtime overhead