Installation
bun add hono @hono/node-server
Import the Hono integration:
import {
registerHonoRoutes,
onionErrorHandler,
} from '@cosmneo/onion-lasagna/http/frameworks/hono';
Quick Start
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { registerHonoRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/hono';
import { bootstrapProjectManagement } from './bootstrap';
// Bootstrap your bounded context
const { routes } = bootstrapProjectManagement();
const app = new Hono();
// Apply error handler
app.onError((err, c) => onionErrorHandler(err, c));
// Register routes
registerHonoRoutes(app, routes);
serve({ fetch: app.fetch, port: 3000 });
Registering Routes
The registerHonoRoutes function registers routes created with serverRoutes():
import { serverRoutes } from '@cosmneo/onion-lasagna/http/server';
import { registerHonoRoutes } from '@cosmneo/onion-lasagna/http/frameworks/hono';
// 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 Hono
registerHonoRoutes(app, routes);
Options
interface RegisterHonoRoutesOptions {
prefix?: string; // Route prefix (e.g., '/api/v1')
middlewares?: HonoMiddleware[]; // Middlewares applied to all routes
contextExtractor?: HonoContextExtractor; // Extract auth context
}
registerHonoRoutes(app, routes, {
prefix: '/api',
middlewares: [authMiddleware],
contextExtractor: (c) => ({
userId: c.get('jwtPayload')?.sub,
}),
});
Context Extraction (Protected Routes)
For authenticated routes, provide a contextExtractor to pass auth data to handlers:
import { jwt } from 'hono/jwt';
// JWT middleware
const authMiddleware = jwt({
secret: process.env.JWT_SECRET,
});
// Define context type
interface AuthContext {
userId: string;
}
// Register with context extraction
registerHonoRoutes(app, routes, {
middlewares: [authMiddleware],
contextExtractor: (c): AuthContext => ({
userId: c.get('jwtPayload')?.sub ?? '',
}),
});
The context is passed to your handler's requestMapper:
.handle('projects.create', {
requestMapper: (req, ctx) => ({
name: req.body.name,
createdBy: ctx.userId, // From contextExtractor
}),
useCase: createProjectUseCase,
responseMapper: (output) => ({
status: 201 as const,
body: { projectId: output.projectId },
}),
})
Error Handler
The onionErrorHandler maps domain errors to HTTP responses:
app.onError((err, c) => onionErrorHandler(err, c));
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 |
Warning:
Masked errors return { "message": "An unexpected error occurred", "errorCode": "INTERNAL_ERROR" }. Domain and infrastructure errors are masked to prevent leaking implementation details.
Custom Error Handling
Log errors before handling:
app.onError((err, c) => {
console.error('Error:', err);
return onionErrorHandler(err, c);
});
Complete Example
import 'dotenv/config';
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { jwt } from 'hono/jwt';
import { registerHonoRoutes, onionErrorHandler } from '@cosmneo/onion-lasagna/http/frameworks/hono';
import { bootstrapProjectManagement } from './bootstrap';
// Bootstrap
const { routes } = bootstrapProjectManagement();
// Create app
const app = new Hono();
// Middleware
app.use('*', logger());
app.use('*', cors());
// Error handling
app.onError((err, c) => {
console.error('Error:', err);
return onionErrorHandler(err, c);
});
// Health check (no auth)
app.get('/health', (c) => c.json({ status: 'ok' }));
// Auth middleware
const authMiddleware = jwt({
secret: process.env.JWT_SECRET ?? 'dev-secret',
});
// API routes with auth
registerHonoRoutes(app, routes, {
middlewares: [authMiddleware],
contextExtractor: (c) => ({
userId: c.get('jwtPayload')?.sub,
}),
});
// Start server
serve({ fetch: app.fetch, port: 3000 });
console.log('Server running on http://localhost:3000');
Cloudflare Workers
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);
export default app;
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"