Agent skill
backend-controller-pattern-nestjs
Comprehensive NestJS Controller patterns for all controller types. This skill should be used when implementing any controller in NestJS to ensure consistency and use of shared utilities.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/backend-controller-pattern-nestjs-allenlin90-eridu-services-2
Metadata
Additional technical details for this skill
- priority
- 3
- applies to
-
[ "backend", "nestjs", "controllers" ] - supersedes
-
[ "backend-controller-pattern", "backend-controller-pattern-admin", "backend-controller-pattern-me", "backend-controller-pattern-studio", "backend-controller-pattern-backdoor", "backend-controller-pattern-integration" ]
SKILL.md
NestJS Controller Patterns
This skill covers all controller patterns in erify_api, from general principles to module-specific implementations.
Canonical Examples
Study these real implementations as the source of truth:
- Admin: admin-client.controller.ts
- Studio: studio-task-template.controller.ts
- Base Controllers: base-admin.controller.ts, base-studio.controller.ts, base.controller.ts
Detailed code examples: See references/controller-examples.md
Core Responsibilities
ALL controllers share these responsibilities:
- Accept HTTP requests (Method, Body, Query, Headers)
- Validate input (Check format, required fields)
- Translate DTOs (Convert external format → internal service payloads)
- Call Service Layer (Delegate business logic)
- Serialize Response (Transform/Filter data)
- Handle Errors (Map exceptions to HTTP Status codes)
Shared Principles
1. Response Serialization
🔴 Critical: ALL endpoints must use Zod for response serialization to ensure no internal data (like database IDs) leaks.
- Use
@ZodResponse(Schema, Status)for standard responses - Use
@ZodPaginatedResponse(Schema)for list endpoints
@Get(':id')
@ZodResponse(UserDto)
async getUser(...) { ... }
2. Validation Pipes
🟡 Recommended: Always use UidValidationPipe for validating uid parameters.
@Param('id', new UidValidationPipe(UserService.UID_PREFIX, 'User'))
id: string
3. DTO Standards
- Request DTOs: Define validation rules using
zod - Response DTOs: Define output shape, excluding sensitive fields
- Pagination: Use
PaginationQueryDtofrom@/lib/pagination/pagination.schema
4. HTTP Status Codes
| Method | Success Code | Decorator Implementation |
|---|---|---|
GET |
200 OK | Default / @ZodResponse(S, HttpStatus.OK) |
POST |
201 Created | @ZodResponse(S, HttpStatus.CREATED) |
PATCH |
200 OK | @ZodResponse(S, HttpStatus.OK) |
DELETE |
204 No Content | @ZodResponse(undefined, HttpStatus.NO_CONTENT) |
5. Payload Translation & Property Filtering
🔴 Critical: Controllers MUST adapt external DTOs to internal Service Payloads and filter unnecessary properties.
Why:
- Services should be decoupled from HTTP layer and context-agnostic
- Services define clean contracts for exactly what they need
- DTOs may contain extra fields (pagination, UI state, metadata) that services don't need
- Passing entire DTOs couples services to API structure changes
Rule: ALWAYS extract only the properties the service contract requires. NEVER pass entire DTO objects.
@Post()
async create(@Param('orgId') orgId: string, @Body() dto: CreateUserDto) {
// ✅ GOOD: Extract ONLY what service needs
const { name, email } = dto;
// Filtered out: dto.pageSize, dto.sortOrder, etc.
return this.userService.create({
name,
email,
org: { connect: { uid: orgId } }
});
}
// ❌ BAD: Pass entire DTO
async create(@Body() dto: CreateUserDto) {
return this.userService.create(dto); // Service now knows about ALL DTO fields
}
// ❌ BAD: Spread operator without explicit filtering
async create(@Body() dto: CreateUserDto) {
return this.userService.create({ ...dto }); // Same problem
}
// ❌ BAD: Deleting properties
async create(@Body() dto: CreateUserDto) {
delete dto.pageSize; // Mutating DTO, not explicit about what service needs
return this.userService.create(dto);
}
Pattern for complex DTOs:
// DTO may have many fields for validation/UI purposes
interface CreateTaskDto {
name: string;
description: string;
assigneeId?: string;
// Extra fields controllers use but services don't need:
returnUrl?: string; // UI navigation
skipNotification?: boolean; // HTTP-specific flag
}
@Post()
async create(@Body() dto: CreateTaskDto) {
// ✅ Extract only service contract fields
const { name, description, assigneeId } = dto;
const task = await this.taskService.create({
name,
description,
assigneeId,
});
// Controller handles HTTP-specific logic
if (dto.skipNotification) {
// Controller decision, not service concern
}
return task;
}
6. Layer Boundaries
🟡 Recommended: Maintain strict separation of concerns.
[ HTTP Controller ] <-- Knows about Requests, Responses, Status Codes
|
v
[ Business Service ] <-- Knows about Logic, Transactions, Domain Errors
|
v
[ Data Repository ] <-- Knows about Database, SQL, ORM
Anti-Patterns:
- ❌ Controller running SQL queries (Leaky abstraction)
- ❌ Controller containing complex logic (Fat controller)
- ❌ Service returning HTTP objects (Service coupled to transport)
7. Pagination
🟡 Recommended: Always limit lists to prevent DoS and performance issues.
Standard Response Format:
{
"data": [ ... ],
"meta": {
"page": 1,
"limit": 10,
"total": 150
}
}
Admin Controllers
Use Case: System admin endpoints for managing resources across the entire system.
Core Principles
- 🔴 Critical: All admin controllers MUST extend
BaseAdminController - 🔴 Critical: Automatically protected by
@AdminProtected()via the base class - 🟡 Recommended: Use
@AdminResponse()and@AdminPaginatedResponse()instead of generic Zod decorators - 🟡 Recommended: All routes must start with
admin/
Base Controller Features
BaseAdminController provides:
@AdminProtected()decorator applicationcreatePaginatedResponse()helperensureResourceExists()andensureFieldExists()helpers
Checklist
- Controller extends
BaseAdminController - Route prefix is
admin/<resource> - Uses
@AdminResponse/@AdminPaginatedResponse - Uses
UidValidationPipefor ID parameters - Uses
ensureResourceExistsfor 404 checks
Studio Controllers
Use Case: Studio-scoped endpoints for resources that belong to a specific studio.
Core Principles
- 🔴 Critical: Extend
BaseStudioController - 🔴 Critical: Path structure must be
studios/:studioId/resource - 🔴 Critical: All queries must filter by studio context
- 🟡 Recommended: Use
@ZodResponse()and@ZodPaginatedResponse()
Authorization
BaseStudioController automatically requires studio membership via @StudioProtected().
Add role restrictions at class or method level:
import { STUDIO_ROLE } from '@eridu/api-types/memberships';
import { StudioProtected } from '@/lib/decorators/studio-protected.decorator';
// All endpoints require ADMIN
@StudioProtected([STUDIO_ROLE.ADMIN])
@Controller('studios/:studioId/task-templates')
export class StudioTaskTemplateController extends BaseStudioController { }
// Mixed: default membership, admin for delete
@Controller('studios/:studioId/resource')
export class ResourceController extends BaseStudioController {
@Get() list() { } // Any member
@StudioProtected([STUDIO_ROLE.ADMIN])
@Delete(':id') delete() { } // Admin only
}
Available roles: STUDIO_ROLE.ADMIN, STUDIO_ROLE.MEMBER
Quick Reference
| Pattern | Code |
|---|---|
| Studio scoping | studioUid: studioId (list), studio: { uid: studioId } (findOne/update/delete) |
| UID validation | @Param('studioId', new UidValidationPipe(StudioService.UID_PREFIX, 'Studio')) |
| Studio relation | studio: { connect: { uid: studioId } } (create) |
| DTO extraction | const { name, description } = dto; then pass to service |
Checklist
- Extends
BaseStudioController - Route:
studios/:studioId/resource - Authorization:
@StudioProtected([roles])if role restrictions needed - UID validation on
studioIdand resourceid - Studio scoping in all queries
- Create operations connect studio relation
User (Me) Controllers
Use Case: Authenticated users interacting with their own resources.
Core Principles
- 🟡 Recommended: Standard NestJS controller (no specific base class required)
- 🔴 Critical: ALWAYS use
@CurrentUser()to scope operations to the authenticated user - 🟡 Recommended: Routes typically start with
me/or implied user context
Checklist
- Route starts with
me/or is user-scoped - Uses
@CurrentUser()to get user ID - 🔴 Critical: NEVER trusts user ID from request body/params for self-operations
- Uses
@ZodResponseor@ZodPaginatedResponse
Backdoor Controllers
Use Case: Service-to-service communication or internal tools using API Key authentication.
Core Principles
- 🔴 Critical: All backdoor controllers MUST extend
BaseBackdoorController - 🔴 Critical: Automatically authenticated via API Key using the
@Backdoor()decorator (from base class) - 🟡 Recommended: All routes must start with
backdoor/
Checklist
- Controller extends
BaseBackdoorController - Route prefix is
backdoor/<resource> - Uses
@ZodResponsefor serialization - NO
@CurrentUserdecorator (concept doesn't exist for API keys)
Integration Controllers
Use Case: External integrations like Google Sheets extensions or webhooks.
Core Principles
- 🟡 Recommended: Integration controllers should extend their specific base class (e.g.,
BaseGoogleSheetsController) - 🟡 Recommended: Use specific decorators for the integration type (e.g.,
@GoogleSheets()) - 🟡 Recommended: Response format often requires specific serialization compatibility (e.g., snake_case for external tools)
Checklist
- Controller extends appropriate base (e.g.,
BaseGoogleSheetsController) - Uses specific auth decorator (e.g.,
@GoogleSheets) - Uses
@ZodSerializerDtofor strict output serialization
Best Practices Summary
- Choose the correct controller type (Admin/Studio/Me/Backdoor/Integration)
- Extend the appropriate base controller
- Use Zod serialization for ALL outputs with
@ZodResponse - Use
UidValidationPipefor all UIDs - 🔴 Critical: Translate DTOs into typed Service Payloads (never pass DTOs directly)
- Apply proper authorization decorators
- Scope queries appropriately (studio/user context)
- Document all endpoints via decorators
- Use correct HTTP status codes
- Implement pagination for list endpoints
Related Skills
- Service Pattern NestJS - Service layer patterns
- Data Validation - Input validation and serialization
- Shared API Types - API contracts and schemas
- Database Patterns - Soft delete, transactions
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?