Claude-skill-registry ddd-api-generator
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").
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/ddd-api-generator" ~/.claude/skills/majiayu000-claude-skill-registry-ddd-api-generator && rm -rf "$T"
skills/data/ddd-api-generator/SKILL.mdDDD 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
, etc.)@IsEmail - Response Serializers: Separate classes with
decorators for Swagger docs@JSONSchema - Controllers: Decorator-based routing with
,@JsonController
,@Get
, etc.@Post - OpenAPI Docs: Complete Swagger documentation using
and@OpenAPI@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:
not/v1/users/v1/user - Lowercase with hyphens:
/v1/sms-messages - No verbs:
not/v1/users/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:
- for string fields@IsString()
- for boolean fields@IsBoolean()
- for number fields@IsNumber()
- for Date fields@IsDate()
- for array fields@IsArray()
- for optional/nullable fields@IsOptional()
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:
→ 404NotFoundError
→ 409ConflictError
→ 400ValidationError
→ 401UnauthorizedError
→ 403ForbiddenError
→ 422DomainError
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
decorators@JSONSchema - Use
for routing@JsonController - Use route decorators:
,@Get
,@Post
,@Patch@Delete - Use
to inject authenticated user@CurrentUser() - Use
for authorization@RequirePermissions() - Add
and@OpenAPI()
to all endpoints@ResponseSchema() - Use
for POST endpoints@HttpCode(201) - 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
or@OpenAPI()
decorators@ResponseSchema() - Forget
on DTO/Serializer fields@JSONSchema() - 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
decorators@JSONSchema - Controller has
and@injectable()@JsonController() - Use cases injected (not repositories)
- Route decorators used:
,@Get
,@Post
,@Patch@Delete -
metadata complete@OpenAPI() -
applied@ResponseSchema() - All response codes documented
- Tags assigned
- Pagination implemented for lists
-
added where needed@RequirePermissions()
Related Skills
- ddd-usecase-generator: Generate use cases called by controllers
- api-validator: Validate API standards compliance
- ddd-validator: Validate overall DDD compliance