Installation
bun add @nestjs/core @nestjs/common @nestjs/platform-express
Import the NestJS integration:
import {
OnionRequest,
OnionExceptionFilter,
type ContextualRawHttpRequest,
type NestContextExtractor,
} from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
Overview
NestJS integration differs from other frameworks because NestJS uses decorators and its own module system. The integration provides:
OnionRequest- Decorator to extract HTTP request dataOnionExceptionFilter- Exception filter for domain errors- Type-safe context extraction for authenticated routes
Quick Start
import { Controller, Get, Post, UseGuards, UseFilters } from '@nestjs/common';
import {
OnionRequest,
OnionExceptionFilter,
type ContextualRawHttpRequest,
} from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
import type { RawHttpRequest, UnifiedRouteInput } from '@cosmneo/onion-lasagna/http/server';
@Controller('api/users')
@UseFilters(OnionExceptionFilter)
export class UsersController {
constructor(private readonly routeHandlers: Map<string, UnifiedRouteInput>) {}
@Get(':userId')
async getUser(@OnionRequest() request: RawHttpRequest) {
const route = this.routeHandlers.get('GET:/api/users/{userId}');
return route.handler(request);
}
}
@OnionRequest Decorator
Extracts an HttpRequest object from the NestJS request:
import { OnionRequest } from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
import type { RawHttpRequest } from '@cosmneo/onion-lasagna/http/server';
@Get(':id')
async getItem(@OnionRequest() request: RawHttpRequest) {
// request contains: pathParams, query, body, headers
}
The RawHttpRequest structure (from @cosmneo/onion-lasagna/http/server):
interface RawHttpRequest {
pathParams: Record<string, string>;
query: Record<string, string | string[]>;
body: unknown;
headers: Record<string, string>;
}
With Context Extractor (Protected Routes)
For routes that need authentication context, pass a context extractor:
import { Controller, Get, UseGuards } from '@nestjs/common';
import type { ExecutionContext } from '@nestjs/common';
import {
OnionRequest,
type NestContextExtractor,
type ContextualRawHttpRequest,
} from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
interface AuthContext {
userId: string;
}
const extractAuthContext: NestContextExtractor<AuthContext> = (ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return {
userId: request.user?.userId ?? '',
};
};
@Controller('api/projects')
@UseGuards(JwtAuthGuard)
export class ProjectsController {
@Get()
async list(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
) {
// request.context.userId is type-safe
}
}
OnionExceptionFilter
Maps domain errors to HTTP responses:
import { UseFilters } from '@nestjs/common';
import { OnionExceptionFilter } from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
// Apply to controller
@Controller('users')
@UseFilters(OnionExceptionFilter)
export class UsersController {}
// Or apply globally in main.ts
app.useGlobalFilters(new OnionExceptionFilter());
Error Mapping
| Error Type | HTTP Status |
|---|---|
ObjectValidationError | 400 |
InvalidRequestError | 400 |
UseCaseError | 400 |
AccessDeniedError | 403 |
NotFoundError | 404 |
ConflictError | 409 |
UnprocessableError | 422 |
| Internal errors | 500 |
Using Unified Route Handlers
For consistency with other frameworks, you can use the unified route handlers:
import { Controller, Get, Post, Patch, Delete, Inject, UseGuards, Res, HttpCode } from '@nestjs/common';
import type { Response } from 'express';
import {
OnionRequest,
type ContextualRawHttpRequest,
type NestContextExtractor,
} from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
import type { UnifiedRouteInput } from '@cosmneo/onion-lasagna/http/server';
interface AuthContext {
userId: string;
}
const extractAuthContext: NestContextExtractor<AuthContext> = (ctx) => {
const request = ctx.switchToHttp().getRequest();
return { userId: request.user?.userId ?? '' };
};
@Controller('api/projects')
@UseGuards(JwtAuthGuard)
export class ProjectsController {
constructor(
@Inject('ROUTE_HANDLERS')
private readonly routeHandlers: Map<string, UnifiedRouteInput>,
) {}
private async callHandler(
method: string,
path: string,
request: ContextualRawHttpRequest<AuthContext>,
res: Response,
) {
const route = this.routeHandlers.get(`${method}:${path}`);
if (!route) throw new Error(`Route not found: ${method}:${path}`);
const response = await route.handler(request, request.context);
res.status(response.status);
return response.body;
}
@Post()
async create(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
@Res({ passthrough: true }) res: Response,
) {
return this.callHandler('POST', '/api/projects', request, res);
}
@Get()
async list(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
@Res({ passthrough: true }) res: Response,
) {
return this.callHandler('GET', '/api/projects', request, res);
}
}
Module Setup
import { Module } from '@nestjs/common';
import { ProjectsController } from './projects.controller';
import { bootstrapProjectManagement } from '@repo/backend/bounded-contexts/project-management';
const { routes } = bootstrapProjectManagement();
// Create a map of routes for injection
const routeHandlersMap = new Map(
routes.map((route) => [`${route.method}:${route.path}`, route]),
);
@Module({
controllers: [ProjectsController],
providers: [
{
provide: 'ROUTE_HANDLERS',
useValue: routeHandlersMap,
},
],
})
export class ProjectsModule {}
Global Setup
Configure the exception filter globally in main.ts:
import { NestFactory } from '@nestjs/core';
import { OnionExceptionFilter } from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global exception filter
app.useGlobalFilters(new OnionExceptionFilter());
await app.listen(3000);
}
bootstrap();
Complete Example
// main.ts
import 'dotenv/config';
import { NestFactory } from '@nestjs/core';
import { OnionExceptionFilter } from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
app.useGlobalFilters(new OnionExceptionFilter());
await app.listen(process.env.PORT ?? 3000);
console.log(`Server running on http://localhost:${process.env.PORT ?? 3000}`);
}
bootstrap();
// projects.controller.ts
import { Controller, Get, Post, Patch, Delete, UseGuards, Inject, Res, HttpCode } from '@nestjs/common';
import type { Response } from 'express';
import {
OnionRequest,
type ContextualRawHttpRequest,
type NestContextExtractor,
} from '@cosmneo/onion-lasagna/http/frameworks/nestjs';
import type { UnifiedRouteInput } from '@cosmneo/onion-lasagna/http/server';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
interface AuthContext {
userId: string;
}
const extractAuthContext: NestContextExtractor<AuthContext> = (ctx) => {
const request = ctx.switchToHttp().getRequest();
return { userId: request.user?.userId ?? '' };
};
@Controller('api/projects')
@UseGuards(JwtAuthGuard)
export class ProjectsController {
constructor(
@Inject('ROUTE_HANDLERS')
private readonly routeHandlers: Map<string, UnifiedRouteInput>,
) {}
private async callHandler(
method: string,
path: string,
request: ContextualRawHttpRequest<AuthContext>,
res: Response,
) {
const route = this.routeHandlers.get(`${method}:${path}`);
if (!route) throw new Error(`Route not found: ${method}:${path}`);
const response = await route.handler(request, request.context);
res.status(response.status);
if (response.headers) {
for (const [key, value] of Object.entries(response.headers)) {
res.setHeader(key, value);
}
}
return response.body;
}
@Post()
async create(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
@Res({ passthrough: true }) res: Response,
) {
return this.callHandler('POST', '/api/projects', request, res);
}
@Get()
async list(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
@Res({ passthrough: true }) res: Response,
) {
return this.callHandler('GET', '/api/projects', request, res);
}
@Get(':projectId')
async get(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
@Res({ passthrough: true }) res: Response,
) {
return this.callHandler('GET', '/api/projects/{projectId}', request, res);
}
@Delete(':projectId')
@HttpCode(204)
async delete(
@OnionRequest(extractAuthContext) request: ContextualRawHttpRequest<AuthContext>,
@Res({ passthrough: true }) res: Response,
) {
return this.callHandler('DELETE', '/api/projects/{projectId}', request, res);
}
}