name: api-design description: Design RESTful API endpoints following project conventions including Swagger, guards, and error handling.
API Design
Use this skill when creating new API endpoints or reviewing existing API design.
1. RESTful Conventions
URL Structure
All API routes use the /api prefix:
/api/<resource> # Collection
/api/<resource>/:id # Single item by ID
/api/<resource>/:slug # Single item by slug (for SEO-friendly URLs)
HTTP Methods
| Method | Purpose | Example | Response Code |
|---|---|---|---|
| GET | Retrieve resource | GET /api/blog |
200 |
| POST | Create resource | POST /api/blog |
201 |
| PUT | Update resource | PUT /api/blog/:id |
200 |
| PATCH | Partial update | PATCH /api/blog/:id |
200 |
| DELETE | Delete resource | DELETE /api/blog/:id |
204 |
Naming Conventions
- Use plural nouns for resources:
/api/posts, not/api/post - Use kebab-case for multi-word resources:
/api/blog-posts - Nest sub-resources:
/api/posts/:postId/comments
2. Controller Structure
Basic Controller Template
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
UseGuards,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { AuthGuardAdapter } from '../../shared/adapters/auth';
@ApiTags('Blog')
@Controller('api/blog')
export class BlogController {
constructor(private readonly blogService: BlogService) {}
@Get()
@ApiOperation({ summary: 'Get all blog posts' })
@ApiResponse({ status: 200, description: 'Returns all blog posts' })
findAll() {
return this.blogService.findAll();
}
@Get(':slug')
@ApiOperation({ summary: 'Get a blog post by slug' })
@ApiResponse({ status: 200, description: 'Returns the blog post' })
@ApiResponse({ status: 404, description: 'Post not found' })
findBySlug(@Param('slug') slug: string) {
return this.blogService.findBySlug(slug);
}
@Post()
@UseGuards(AuthGuardAdapter)
@ApiBearerAuth()
@ApiOperation({ summary: 'Create a new blog post' })
@ApiResponse({ status: 201, description: 'Post created successfully' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
create(@Body() dto: CreateBlogPostDto) {
return this.blogService.create(dto);
}
}
3. Required Swagger Decorators
Every endpoint MUST have these decorators:
Class Level
| Decorator | Purpose | Example |
|---|---|---|
@ApiTags |
Group in Swagger UI | @ApiTags('Blog') |
@Controller |
Define route prefix | @Controller('api/blog') |
Method Level
| Decorator | Purpose | Required |
|---|---|---|
@ApiOperation |
Describe what endpoint does | ✅ Yes |
@ApiResponse |
Document response codes | ✅ Yes |
@ApiBearerAuth |
Mark as requiring auth | If guarded |
Example: Complete Swagger Documentation
@Post()
@UseGuards(AuthGuardAdapter)
@ApiBearerAuth()
@ApiOperation({
summary: 'Create a new blog post',
description: 'Creates a new blog post. Requires authentication.',
})
@ApiResponse({
status: 201,
description: 'The blog post has been created successfully.',
type: BlogPost,
})
@ApiResponse({
status: 400,
description: 'Invalid input data.',
})
@ApiResponse({
status: 401,
description: 'Unauthorized - valid JWT token required.',
})
create(@Body() dto: CreateBlogPostDto): Promise<BlogPost> {
return this.blogService.create(dto);
}
4. Authentication & Guards
Protecting Routes
Use AuthGuardAdapter for routes requiring authentication (see ADR-005 for hexagonal architecture details):
import { AuthGuardAdapter } from '../../shared/adapters/auth';
// Protect single route
@Post()
@UseGuards(AuthGuardAdapter)
create(@Body() dto: CreateDto) { ... }
// Protect entire controller
@UseGuards(AuthGuardAdapter)
@Controller('api/admin')
export class AdminController { ... }
Public vs Protected Endpoints
| Action | Auth Required | Guard |
|---|---|---|
| Read (GET) | Usually No | None |
| Create (POST) | Usually Yes | AuthGuardAdapter |
| Update (PUT) | Usually Yes | AuthGuardAdapter |
| Delete | Usually Yes | AuthGuardAdapter |
5. Error Handling
Standard Exception Classes
Use NestJS built-in exceptions for consistent error responses:
import {
NotFoundException,
BadRequestException,
UnauthorizedException,
ForbiddenException,
ConflictException,
} from '@nestjs/common';
// 404 - Resource not found
throw new NotFoundException(`Post with slug "${slug}" not found`);
// 400 - Invalid input
throw new BadRequestException('Title is required');
// 401 - Not authenticated
throw new UnauthorizedException('Valid token required');
// 403 - Authenticated but not allowed
throw new ForbiddenException('You cannot edit this post');
// 409 - Conflict (e.g., duplicate)
throw new ConflictException('A post with this slug already exists');
Error Response Format
NestJS automatically formats exceptions as:
{
"statusCode": 404,
"message": "Post with slug \"my-post\" not found",
"error": "Not Found"
}
6. DTOs (Data Transfer Objects)
Location
DTOs live in the module's dto/ folder:
src/server/modules/blog/
├── dto/
│ ├── createBlogPost.dto.ts
│ └── updateBlogPost.dto.ts
├── blog.controller.ts
├── blog.service.ts
└── blog.module.ts
DTO Template with Validation
import { IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateBlogPostDto {
@ApiProperty({ description: 'The title of the blog post' })
@IsString()
@IsNotEmpty()
title: string;
@ApiProperty({ description: 'URL-friendly identifier' })
@IsString()
@IsNotEmpty()
slug: string;
@ApiProperty({ description: 'The main content in Markdown' })
@IsString()
@IsNotEmpty()
content: string;
@ApiPropertyOptional({ description: 'Tags for categorization' })
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
}
Partial DTOs for Updates
Use PartialType to make all fields optional for updates:
import { PartialType } from '@nestjs/swagger';
import { CreateBlogPostDto } from './createBlogPost.dto';
export class UpdateBlogPostDto extends PartialType(CreateBlogPostDto) {}
7. Response Optimization
List Endpoints - Select Only Needed Fields
For list endpoints, avoid returning heavy fields:
// In service
async findAll(): Promise<BlogPostSummary[]> {
return this.repository.find({
select: ['id', 'slug', 'title', 'metaDescription', 'publishedAt', 'tags'],
order: { publishedAt: 'DESC' },
});
}
Detail Endpoints - Return Full Object
async findBySlug(slug: string): Promise<BlogPost> {
const post = await this.repository.findOne({ where: { slug } });
if (!post) {
throw new NotFoundException(`Post with slug "${slug}" not found`);
}
return post;
}
8. Checklist for New Endpoints
When adding a new endpoint:
- Controller has
@ApiTagsdecorator - Endpoint has
@ApiOperationwith summary - Endpoint has
@ApiResponsefor all possible status codes - Protected endpoints have
@UseGuards(AuthGuardAdapter)and@ApiBearerAuth - Request body validated with DTO
- DTO has
@ApiPropertydecorators for Swagger - Service throws appropriate exceptions (404, 400, etc.)
- Integration test covers happy path and error cases