DDD API Generator
Generate presentation layer components using routing-controllers with NestJS-style decorators, class-validator for validation, and automatic Swagger documentation.
What This Skill Does
Creates production-ready REST API endpoints:
- •Request DTOs: Validation classes with class-validator decorators (
@IsString,@IsEmail, etc.) - •Response Serializers: Separate classes with
@JSONSchemadecorators for Swagger docs - •Controllers: Decorator-based routing with
@JsonController,@Get,@Post, etc. - •OpenAPI Docs: Complete Swagger documentation using
@OpenAPIand@ResponseSchema - •API Standards: Versioning, naming, status codes, pagination
When to Use This Skill
Use when you need to:
- •Create REST API endpoints for existing use cases
- •Add new routes to existing context
- •Implement paginated list endpoints
- •Build custom API actions
Examples:
- •"Create API endpoints for user management"
- •"Generate product API with search and filtering"
- •"Add order API with status tracking"
API Design Standards
Versioning
All controllers MUST be prefixed with /v1/:
@JsonController('/v1/users')
export class UserController { }
Resource Naming
- •Plural nouns:
/v1/usersnot/v1/user - •Lowercase with hyphens:
/v1/sms-messages - •No verbs:
/v1/usersnot/v1/getUsers
Status Codes
- •200 OK: GET, PATCH, PUT success
- •201 Created: POST success (use
@HttpCode(201)) - •204 No Content: DELETE success
- •400 Bad Request: Validation error
- •401 Unauthorized: Auth required
- •403 Forbidden: Permission denied
- •404 Not Found: Resource not found
- •409 Conflict: Duplicate resource
- •422 Unprocessable Entity: Business rule violation
Response Format
ResponseInterceptor middleware wraps all responses:
{
"success": true,
"data": {...},
"timestamp": "2024-01-15T10:30:00.000Z"
}
Request DTO Pattern
Create request DTOs in dto/requests/ with class-validator decorators:
// dto/requests/create-entity.dto.ts
import { IsString, IsEmail, IsOptional, IsNumber, IsEnum, Length, Min, Max } from 'class-validator';
import { JSONSchema } from 'class-validator-jsonschema';
export class CreateEntityDto {
@IsString()
@Length(1, 100)
@JSONSchema({
description: 'Entity name (1-100 characters)',
minLength: 1,
maxLength: 100,
example: 'My Entity',
})
name!: string;
@IsEmail()
@JSONSchema({
description: 'Valid email address',
format: 'email',
example: 'user@example.com',
})
email!: string;
@IsOptional()
@IsNumber()
@Min(0)
@Max(150)
@JSONSchema({
description: 'Age in years (optional)',
minimum: 0,
maximum: 150,
example: 30,
})
age?: number;
@IsEnum(['admin', 'user', 'guest'])
@JSONSchema({
description: 'User role',
enum: ['admin', 'user', 'guest'],
example: 'user',
})
role!: 'admin' | 'user' | 'guest';
}
Response Serializer Pattern
Create response serializers in dto/responses/ with BOTH class-validator decorators AND @JSONSchema decorators:
⚠️ CRITICAL: Response serializers MUST include class-validator decorators (@IsString(), @IsBoolean(), etc.) for Swagger schema generation. Without these decorators, validationMetadatasToSchemas() cannot generate proper OpenAPI schemas, resulting in generic ["string"] appearing in Swagger instead of the actual response structure.
// dto/responses/entity-response.serializer.ts
import { JSONSchema } from 'class-validator-jsonschema';
import { IsString, IsBoolean, IsDate, IsArray, IsOptional } from 'class-validator';
export class EntityResponseSerializer {
@IsString()
@JSONSchema({
description: 'Entity unique identifier',
format: 'uuid',
example: '550e8400-e29b-41d4-a716-446655440000',
})
id!: string;
@IsString()
@JSONSchema({
description: 'Entity name',
example: 'My Entity',
})
name!: string;
@IsString()
@JSONSchema({
description: 'Email address',
format: 'email',
example: 'user@example.com',
})
email!: string;
@IsBoolean()
@JSONSchema({
description: 'Whether entity is active',
example: true,
})
isActive!: boolean;
@IsArray()
@JSONSchema({
description: 'List of tags',
type: 'array',
items: { type: 'string' },
example: ['tag1', 'tag2'],
})
tags!: string[];
@IsString()
@IsOptional()
@JSONSchema({
description: 'Optional description',
nullable: true,
example: 'Some description',
})
description?: string | null;
@IsDate()
@JSONSchema({
description: 'Creation timestamp',
format: 'date-time',
example: '2024-01-15T10:30:00.000Z',
})
createdAt!: Date;
@IsDate()
@JSONSchema({
description: 'Last update timestamp',
format: 'date-time',
example: '2024-01-15T10:30:00.000Z',
})
updatedAt!: Date;
}
Required class-validator decorators for response serializers:
- •
@IsString()- for string fields - •
@IsBoolean()- for boolean fields - •
@IsNumber()- for number fields - •
@IsDate()- for Date fields - •
@IsArray()- for array fields - •
@IsOptional()- for optional/nullable fields
Controller Pattern
// entity.controller.ts
import { JsonController, Get, Post, Patch, Delete, Body, Param, Query, HttpCode } from 'routing-controllers';
import { ResponseSchema, OpenAPI } from 'routing-controllers-openapi';
import { injectable, inject } from 'tsyringe';
import { CurrentUser, RequirePermissions } from '@/global/decorators';
import { Permission } from '@/global/types';
import type { AuthenticatedUser } from '@/global/types/auth.types';
import {
CreateEntityUseCase,
FindEntityUseCase,
UpdateEntityUseCase,
DeleteEntityUseCase,
ListEntitiesUseCase,
} from '../application';
import { CreateEntityDto } from './dto/requests/create-entity.dto';
import { UpdateEntityDto } from './dto/requests/update-entity.dto';
import { QueryEntityDto } from './dto/requests/query-entity.dto';
import { EntityResponseSerializer } from './dto/responses/entity-response.serializer';
import { EntityListResponseSerializer } from './dto/responses/entity-list-response.serializer';
@injectable()
@JsonController('/v1/entities')
export class EntityController {
constructor(
@inject(CreateEntityUseCase)
private readonly createUseCase: CreateEntityUseCase,
@inject(FindEntityUseCase)
private readonly findUseCase: FindEntityUseCase,
@inject(UpdateEntityUseCase)
private readonly updateUseCase: UpdateEntityUseCase,
@inject(DeleteEntityUseCase)
private readonly deleteUseCase: DeleteEntityUseCase,
@inject(ListEntitiesUseCase)
private readonly listUseCase: ListEntitiesUseCase
) {}
@Post('/')
@HttpCode(201)
@ResponseSchema(EntityResponseSerializer, { statusCode: 201 })
@OpenAPI({
summary: 'Create entity',
description: 'Creates a new entity with the provided data',
tags: ['Entities'],
security: [{ bearerAuth: [] }],
responses: {
'201': { description: 'Entity created successfully' },
'400': { description: 'Invalid input data' },
'401': { description: 'Unauthorized' },
'403': { description: 'Forbidden - insufficient permissions' },
'409': { description: 'Entity already exists' },
},
})
@RequirePermissions(Permission.ENTITIES_WRITE)
async create(
@CurrentUser() user: AuthenticatedUser,
@Body() body: CreateEntityDto
): Promise<EntityResponseSerializer> {
const result = await this.createUseCase.execute({
...body,
tenantId: user.tenantId,
});
return {
id: result.id,
name: result.name,
email: result.email,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
};
}
@Get('/:id')
@ResponseSchema(EntityResponseSerializer)
@OpenAPI({
summary: 'Get entity by ID',
description: 'Retrieves a single entity by its unique identifier',
tags: ['Entities'],
security: [{ bearerAuth: [] }],
responses: {
'200': { description: 'Entity found' },
'401': { description: 'Unauthorized' },
'403': { description: 'Forbidden - insufficient permissions' },
'404': { description: 'Entity not found' },
},
})
@RequirePermissions(Permission.ENTITIES_READ)
async findById(
@Param('id') id: string,
@CurrentUser() user: AuthenticatedUser
): Promise<EntityResponseSerializer> {
const entity = await this.findUseCase.execute(id);
return {
id: entity.id,
name: entity.name,
email: entity.email,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
};
}
@Get('/')
@ResponseSchema(EntityListResponseSerializer)
@OpenAPI({
summary: 'List entities',
description: 'Retrieves a paginated list of entities',
tags: ['Entities'],
security: [{ bearerAuth: [] }],
responses: {
'200': { description: 'Entities retrieved successfully' },
'401': { description: 'Unauthorized' },
'403': { description: 'Forbidden - insufficient permissions' },
},
})
@RequirePermissions(Permission.ENTITIES_READ)
async list(
@Query() query: QueryEntityDto,
@CurrentUser() user: AuthenticatedUser
): Promise<EntityListResponseSerializer> {
const result = await this.listUseCase.execute({
...query,
tenantId: user.tenantId,
});
return {
items: result.items.map((entity) => ({
id: entity.id,
name: entity.name,
email: entity.email,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
})),
total: result.total,
limit: result.limit,
offset: result.offset,
};
}
@Patch('/:id')
@ResponseSchema(EntityResponseSerializer)
@OpenAPI({
summary: 'Update entity',
description: 'Updates an existing entity with partial data',
tags: ['Entities'],
security: [{ bearerAuth: [] }],
responses: {
'200': { description: 'Entity updated successfully' },
'400': { description: 'Invalid input data' },
'401': { description: 'Unauthorized' },
'403': { description: 'Forbidden - insufficient permissions' },
'404': { description: 'Entity not found' },
},
})
@RequirePermissions(Permission.ENTITIES_WRITE)
async update(
@Param('id') id: string,
@Body() body: UpdateEntityDto,
@CurrentUser() user: AuthenticatedUser
): Promise<EntityResponseSerializer> {
const result = await this.updateUseCase.execute({
id,
...body,
});
return {
id: result.id,
name: result.name,
email: result.email,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
};
}
@Delete('/:id')
@HttpCode(200)
@ResponseSchema(EntityResponseSerializer)
@OpenAPI({
summary: 'Delete entity',
description: 'Permanently deletes an entity',
tags: ['Entities'],
security: [{ bearerAuth: [] }],
responses: {
'200': { description: 'Entity deleted successfully' },
'401': { description: 'Unauthorized' },
'403': { description: 'Forbidden - insufficient permissions' },
'404': { description: 'Entity not found' },
},
})
@RequirePermissions(Permission.ENTITIES_DELETE)
async delete(
@Param('id') id: string,
@CurrentUser() user: AuthenticatedUser
): Promise<{ success: boolean }> {
await this.deleteUseCase.execute(id);
return { success: true };
}
}
Error Handling
Domain errors are automatically mapped to HTTP status codes by GlobalErrorHandler. No explicit try-catch needed in controllers - just let errors bubble up.
The middleware handles:
- •
NotFoundError→ 404 - •
ConflictError→ 409 - •
ValidationError→ 400 - •
UnauthorizedError→ 401 - •
ForbiddenError→ 403 - •
DomainError→ 422
Pagination Pattern
// dto/requests/query-entity.dto.ts
import { IsOptional, IsNumber, IsString, IsEnum, Min } from 'class-validator';
import { JSONSchema } from 'class-validator-jsonschema';
import { Type } from 'class-transformer';
export class QueryEntityDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@JSONSchema({
description: 'Number of items per page',
minimum: 1,
example: 20,
})
limit?: number = 20;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
@JSONSchema({
description: 'Number of items to skip',
minimum: 0,
example: 0,
})
offset?: number = 0;
@IsOptional()
@IsEnum(['name', 'createdAt'])
@JSONSchema({
description: 'Field to sort by',
enum: ['name', 'createdAt'],
example: 'createdAt',
})
sortBy?: 'name' | 'createdAt' = 'createdAt';
@IsOptional()
@IsEnum(['asc', 'desc'])
@JSONSchema({
description: 'Sort order',
enum: ['asc', 'desc'],
example: 'desc',
})
order?: 'asc' | 'desc' = 'desc';
@IsOptional()
@IsString()
@JSONSchema({
description: 'Search term',
example: 'john',
})
search?: string;
}
// dto/responses/entity-list-response.serializer.ts
import { JSONSchema } from 'class-validator-jsonschema';
import { EntityResponseSerializer } from './entity-response.serializer';
export class EntityListResponseSerializer {
@JSONSchema({
description: 'List of entities',
type: 'array',
items: { $ref: '#/components/schemas/EntityResponseSerializer' },
})
items!: EntityResponseSerializer[];
@JSONSchema({
description: 'Total number of entities',
example: 100,
})
total!: number;
@JSONSchema({
description: 'Number of items per page',
example: 20,
})
limit!: number;
@JSONSchema({
description: 'Number of items skipped',
example: 0,
})
offset!: number;
}
Critical Rules
MUST DO:
- •Version all routes with
/v1/ - •Use plural resource names
- •Create separate request DTOs with class-validator decorators
- •Create separate response serializers with
@JSONSchemadecorators - •Use
@JsonControllerfor routing - •Use route decorators:
@Get,@Post,@Patch,@Delete - •Use
@CurrentUser()to inject authenticated user - •Use
@RequirePermissions()for authorization - •Add
@OpenAPI()and@ResponseSchema()to all endpoints - •Use
@HttpCode(201)for POST endpoints - •Implement pagination for list endpoints
- •Document all response status codes
MUST NOT:
- •Skip versioning
- •Use singular resource names
- •Include verbs in resource names
- •Put business logic in controller
- •Return domain entities directly
- •Skip
@OpenAPI()or@ResponseSchema()decorators - •Forget
@JSONSchema()on DTO/Serializer fields - •Skip error handling or validation
Generated Files
/src/contexts/{Context}/presentation/
├── dto/
│ ├── requests/
│ │ ├── create-{entity}.dto.ts
│ │ ├── update-{entity}.dto.ts
│ │ └── query-{entity}.dto.ts
│ └── responses/
│ ├── {entity}-response.serializer.ts
│ └── {entity}-list-response.serializer.ts
└── {context}.controller.ts
Integration
Add controller to /src/main.ts:
import { EntityController } from './contexts/entity/presentation/entity.controller';
const routingControllersOptions = {
controllers: [UserController, TenantController, EntityController],
// ...
};
Validation Checklist
After generation, verify:
- • Routes versioned with
/v1/ - • Plural resource names
- • Lowercase-with-hyphens naming
- • Request DTOs with class-validator decorators
- • Response serializers with
@JSONSchemadecorators - • Controller has
@injectable()and@JsonController() - • Use cases injected (not repositories)
- • Route decorators used:
@Get,@Post,@Patch,@Delete - •
@OpenAPI()metadata complete - •
@ResponseSchema()applied - • All response codes documented
- • Tags assigned
- • Pagination implemented for lists
- •
@RequirePermissions()added where needed
Related Skills
- •ddd-usecase-generator: Generate use cases called by controllers
- •api-validator: Validate API standards compliance
- •ddd-validator: Validate overall DDD compliance