Overview
Creating a new endpoint involves these steps:
- Define the route with schemas
- Add the route to your router
- Create the use case
- Create the handler
- Wire in bootstrap
Steps
1. Define the Route
Create a route definition with schemas for request validation and response documentation:
// presentation/http/routes/users.routes.ts
import { defineRoute } from '@cosmneo/onion-lasagna/http/route';
import { zodSchema, z } from '@cosmneo/onion-lasagna/http/schema/zod';
export const getUserRoute = defineRoute({
method: 'GET',
path: '/api/users/:userId',
request: {
params: {
schema: zodSchema(
z.object({
userId: z.string().uuid(),
}),
),
},
},
responses: {
200: {
description: 'User found',
schema: zodSchema(
z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string(),
}),
),
},
404: {
description: 'User not found',
},
},
docs: {
summary: 'Get user by ID',
tags: ['Users'],
operationId: 'getUser',
},
});
2. Add to Router
Add the route to your router definition:
// presentation/http/router.ts
import { defineRouter } from '@cosmneo/onion-lasagna/http/route';
import { getUserRoute, createUserRoute, listUsersRoute } from './routes/users.routes';
export const userRouter = defineRouter({
users: {
get: getUserRoute,
create: createUserRoute,
list: listUsersRoute,
},
});
3. Create the Use Case
Define the use case port and implementation:
// app/ports/get-user.query.port.ts
interface GetUserQueryPort {
execute(input: GetUserInput): Promise<GetUserOutput>;
}
interface GetUserInput {
userId: string;
}
interface GetUserOutput {
id: string;
email: string;
name: string;
}
// app/use-cases/get-user.query.ts
import { BaseInboundAdapter, NotFoundError } from '@cosmneo/onion-lasagna/backend/core/onion-layers';
class GetUserQuery
extends BaseInboundAdapter<GetUserInput, GetUserOutput>
implements GetUserQueryPort
{
constructor(private readonly userRepo: UserRepositoryPort) {
super();
}
protected async handle(input: GetUserInput): Promise<GetUserOutput> {
const user = await this.userRepo.findById(input.userId);
if (!user) {
throw new NotFoundError({
message: `User ${input.userId} not found`,
code: 'USER_NOT_FOUND',
});
}
return {
id: user.id.value,
email: user.email.value,
name: user.name,
};
}
}
4. Create the Handler
Add the handler using the serverRoutes() builder:
// presentation/http/handlers/users.handlers.ts
import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
import { userRouter } from '../router';
import type { UseCases } from '../../../bootstrap/use-cases.bootstrap';
export function createUserHandlers(useCases: UseCases) {
return serverRoutes(userRouter)
.handle('users.get', {
requestMapper: (req) => ({
userId: req.pathParams.userId,
}),
useCase: useCases.getUserQuery,
responseMapper: (output) => ({
status: 200 as const,
body: {
id: output.id,
email: output.email,
name: output.name,
},
}),
})
.buildPartial();
}
5. Wire in Bootstrap
Add the use case to your bootstrap:
// bootstrap/use-cases.bootstrap.ts
import { GetUserQuery } from '../app/use-cases/get-user.query';
export function createUseCases(adapters: Adapters) {
return {
// ... other use cases
getUserQuery: new GetUserQuery(adapters.userRepository),
};
}
Route Definition Options
Request Configuration
| Property | Description |
|---|---|
request.body.schema | Request body schema |
request.query.schema | Query parameters schema |
request.params.schema | Path parameters schema |
request.headers.schema | Request headers schema |
request.context.schema | Context schema (from middleware) |
Response Configuration
| Property | Description |
|---|---|
responses[status].description | Response description for OpenAPI |
responses[status].schema | Response body schema |
Documentation
| Property | Description |
|---|---|
docs.summary | Short description for OpenAPI |
docs.description | Detailed description |
docs.tags | OpenAPI tags for grouping |
docs.operationId | Unique operation identifier |
Void Operations (DELETE, UPDATE)
For operations that return no content (HTTP 204):
Route Definition
export const deleteUserRoute = defineRoute({
method: 'DELETE',
path: '/api/users/:userId',
request: {
params: {
schema: zodSchema(
z.object({
userId: z.string().uuid(),
}),
),
},
},
responses: {
204: {
description: 'User deleted',
},
404: {
description: 'User not found',
},
},
docs: {
summary: 'Delete a user',
tags: ['Users'],
operationId: 'deleteUser',
},
});
Handler
.handle('users.delete', {
requestMapper: (req) => ({
userId: req.pathParams.userId,
}),
useCase: useCases.deleteUserUseCase,
responseMapper: () => ({
status: 204 as const,
body: undefined,
}),
})
Protected Endpoints
For endpoints requiring authentication, add a context schema:
Route with Context
export const createPostRoute = defineRoute({
method: 'POST',
path: '/api/posts',
request: {
body: {
schema: zodSchema(
z.object({
title: z.string().min(1),
content: z.string(),
}),
),
},
context: {
schema: zodSchema(
z.object({
userId: z.string(),
}),
),
},
},
responses: {
201: {
description: 'Post created',
schema: zodSchema(z.object({ postId: z.string() })),
},
},
});
Handler with Context
.handle('posts.create', {
requestMapper: (req, ctx) => ({
title: req.body.title,
content: req.body.content,
authorId: ctx.userId, // From context schema
}),
useCase: useCases.createPostUseCase,
responseMapper: (output) => ({
status: 201 as const,
body: { postId: output.postId },
}),
})
The context is provided by the contextExtractor in framework registration:
registerHonoRoutes(app, routes, {
middlewares: [authMiddleware],
contextExtractor: (c) => ({
userId: c.get('jwtPayload')?.sub,
}),
});
Checklist
- Route definition created with schemas
- Route added to router
- Use case port defined
- Use case implemented with
BaseInboundAdapter - Handler added using
serverRoutes()builder - Use case wired in bootstrap
- Routes exported from bootstrap