| name | ddd-api-generator |
| description | Generate REST API endpoints with class-validator DTOs, routing-controllers decorators, and complete Swagger docs. Use when creating API endpoints for existing use cases, adding routes, or building custom API actions (e.g., "Create user API", "Generate product endpoints"). |
| allowed-tools | Read, Write, Edit, Glob, Grep |
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→ 404ConflictError→ 409ValidationError→ 400UnauthorizedError→ 401ForbiddenError→ 403DomainError→ 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