🔧 Backend API Development Skill
Philosophy: APIs are contracts. Build them right the first time.
When to Use
Use this skill when:
- Creating a new API endpoint
- Building a new service/module
- Refactoring existing API code
- Adding new functionality to backend
- Need to follow RESTful/GraphQL best practices
Do NOT use this skill when:
- Just fixing a small bug (use debugging skill)
- Only modifying frontend (use frontend skill)
- Database-only changes (use database skill)
Prerequisites
Before starting:
Process
Phase 1: DESIGN 📐
Goal: Design the API before writing code.
Steps:
Define the Resource
resource:
name: User
description: Represents a platform user
domain: authentication
Design Endpoints (REST)
endpoints:
- method: GET
path: /users
description: List all users
query_params: [page, limit, search]
response: User[]
- method: GET
path: /users/:id
description: Get single user
response: User
- method: POST
path: /users
description: Create new user
body: CreateUserDto
response: User
- method: PUT
path: /users/:id
description: Update user
body: UpdateUserDto
response: User
- method: DELETE
path: /users/:id
description: Delete user
response: void
Define DTOs (Data Transfer Objects)
// CreateUserDto
interface CreateUserDto {
email: string; // required, email format
password: string; // required, min 8 chars
name: string; // required, min 2 chars
role?: UserRole; // optional, default: 'user'
}
// UpdateUserDto
type UpdateUserDto = Partial<CreateUserDto>;
// UserResponseDto
interface UserResponseDto {
id: string;
email: string;
name: string;
role: UserRole;
createdAt: DateTime;
updatedAt: DateTime;
// Note: password NOT included
}
Plan Error Responses
errors:
- code: 400
when: Invalid input
response: { message, errors: [{field, message}] }
- code: 401
when: Not authenticated
response: { message: "Unauthorized" }
- code: 403
when: Not authorized
response: { message: "Forbidden" }
- code: 404
when: Resource not found
response: { message: "User not found" }
- code: 409
when: Conflict (e.g., email exists)
response: { message: "Email already registered" }
Output: Complete API design document.
Phase 2: STRUCTURE 🏗️
Goal: Set up the file structure.
NestJS Structure:
src/
└── users/
├── users.module.ts # Module definition
├── users.controller.ts # HTTP layer
├── users.service.ts # Business logic
├── users.repository.ts # Data access (optional)
├── dto/
│ ├── create-user.dto.ts
│ ├── update-user.dto.ts
│ └── user-response.dto.ts
├── entities/
│ └── user.entity.ts
├── guards/
│ └── user-owner.guard.ts
└── users.controller.spec.ts
FastAPI Structure:
app/
└── users/
├── __init__.py
├── router.py # Routes
├── service.py # Business logic
├── repository.py # Data access
├── schemas.py # Pydantic models
├── models.py # SQLAlchemy models
└── dependencies.py # Dependency injection
Phase 3: IMPLEMENTATION 💻
Goal: Implement the API layer by layer.
Order of Implementation:
Entity/Model First
// user.entity.ts
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
@IsEmail()
email: string;
@Column()
@Exclude() // Never expose password
password: string;
@Column()
name: string;
@Column({ default: 'user' })
role: UserRole;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
DTOs with Validation
// create-user.dto.ts
export class CreateUserDto {
@IsEmail()
@Transform(({ value }) => value.toLowerCase().trim())
email: string;
@IsString()
@MinLength(8)
@Matches(/^(?=.*[A-Za-z])(?=.*\d)/, {
message: 'Password must contain letters and numbers'
})
password: string;
@IsString()
@MinLength(2)
@MaxLength(50)
name: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole;
}
Service Layer (Business Logic)
// users.service.ts
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async create(dto: CreateUserDto): Promise<User> {
// Check for existing email
const existing = await this.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already registered');
}
// Hash password
const hashedPassword = await bcrypt.hash(dto.password, 10);
// Create and save
const user = this.usersRepository.create({
...dto,
password: hashedPassword,
});
return this.usersRepository.save(user);
}
async findAll(options: PaginationOptions): Promise<PaginatedResult<User>> {
// Implementation with pagination
}
// ... other methods
}
Controller (HTTP Layer)
// users.controller.ts
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
const user = await this.usersService.create(dto);
return plainToInstance(UserResponseDto, user);
}
@Get()
@UseGuards(AuthGuard)
async findAll(
@Query() query: PaginationQueryDto
): Promise<PaginatedResult<UserResponseDto>> {
return this.usersService.findAll(query);
}
@Get(':id')
@UseGuards(AuthGuard)
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<UserResponseDto> {
const user = await this.usersService.findOne(id);
if (!user) {
throw new NotFoundException('User not found');
}
return plainToInstance(UserResponseDto, user);
}
// ... other endpoints
}
Phase 4: SECURITY 🔒
Goal: Ensure API is secure.
Security Checklist:
Input Validation
Authentication
Authorization
Data Protection
Rate Limiting
SQL Injection Prevention
Phase 5: TESTING 🧪
Goal: Write comprehensive tests.
Test Types:
Unit Tests
describe('UsersService', () => {
describe('create', () => {
it('should create a new user', async () => {
const dto = { email: 'test@example.com', ... };
const result = await service.create(dto);
expect(result.email).toBe(dto.email);
});
it('should hash the password', async () => {
const dto = { password: 'plaintext', ... };
const result = await service.create(dto);
expect(result.password).not.toBe(dto.password);
});
it('should throw on duplicate email', async () => {
// Setup: create user first
await service.create({ email: 'test@example.com', ... });
// Act & Assert
await expect(
service.create({ email: 'test@example.com', ... })
).rejects.toThrow(ConflictException);
});
});
});
Integration Tests
describe('Users API', () => {
it('POST /users should create user', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', password: 'Password1', name: 'Test' })
.expect(201);
expect(response.body.email).toBe('test@example.com');
expect(response.body.password).toBeUndefined();
});
it('GET /users should require auth', async () => {
await request(app.getHttpServer())
.get('/users')
.expect(401);
});
});
Phase 6: DOCUMENTATION 📝
Goal: Document the API.
OpenAPI/Swagger:
@ApiTags('users')
@Controller('users')
export class UsersController {
@Post()
@ApiOperation({ summary: 'Create a new user' })
@ApiResponse({ status: 201, type: UserResponseDto })
@ApiResponse({ status: 400, description: 'Invalid input' })
@ApiResponse({ status: 409, description: 'Email already exists' })
async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
// ...
}
}
Response DTO Documentation:
export class UserResponseDto {
@ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000' })
id: string;
@ApiProperty({ example: 'user@example.com' })
email: string;
@ApiProperty({ example: 'John Doe' })
name: string;
}
Best Practices
API Design
| Practice |
Do |
Don't |
| Naming |
GET /users/:id/orders |
GET /getUserOrders |
| Versioning |
/api/v1/users |
No versioning |
| Pluralization |
/users, /orders |
/user, /order |
| HTTP Methods |
Use correctly (GET=read, POST=create) |
POST for everything |
| Status Codes |
201 for created, 204 for no content |
200 for everything |
Error Handling
// Global exception filter
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message = exception instanceof HttpException
? exception.message
: 'Internal server error';
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
});
}
}
Guidelines
DO ✅
- Design API before coding
- Use proper HTTP methods and status codes
- Validate all inputs
- Handle errors gracefully
- Write tests first (TDD)
- Document with OpenAPI
DON'T ❌
- Expose internal IDs when UUIDs are better
- Return password or sensitive data
- Use GET for mutations
- Skip input validation
- Catch and swallow errors
- Use magic strings/numbers
Success Criteria
Before considering API complete:
Related Skills
skills/kilo-kit/development/database/ - For data layer
skills/kilo-kit/development/security/ - For security concerns
skills/kilo-kit/quality/testing/ - For test coverage
skills/kilo-kit/architecture/system-design/ - For architecture decisions
Backend API Development Skill v1.0.0 — APIs built right
1---2name: backend-api-development3description: Comprehensive backend API development skill for building robust, scalable APIs. Use when creating new endpoints, services, or backend functionality. Keywords: API, backend, endpoint, service, REST, GraphQL, server, controller, route4---56# 🔧 Backend API Development Skill78> **Philosophy:** APIs are contracts. Build them right the first time.910## When to Use1112Use this skill when:13- Creating a new API endpoint14- Building a new service/module15- Refactoring existing API code16- Adding new functionality to backend17- Need to follow RESTful/GraphQL best practices1819**Do NOT use this skill when:**20- Just fixing a small bug (use debugging skill)21- Only modifying frontend (use frontend skill)22- Database-only changes (use database skill)2324---2526## Prerequisites2728Before starting:29- [ ] Requirements are clear (what the API should do)30- [ ] Understand the existing architecture31- [ ] Know the target stack (NestJS, Express, FastAPI, etc.)32- [ ] Database schema exists (or will be created)3334---3536## Process3738### Phase 1: DESIGN 📐3940**Goal:** Design the API before writing code.4142**Steps:**43441. **Define the Resource**45 ```yaml46 resource:47 name: User48 description: Represents a platform user49 domain: authentication50 ```51522. **Design Endpoints (REST)**53 ```yaml54 endpoints:55 - method: GET56 path: /users57 description: List all users58 query_params: [page, limit, search]59 response: User[]60 61 - method: GET62 path: /users/:id63 description: Get single user64 response: User65 66 - method: POST67 path: /users68 description: Create new user69 body: CreateUserDto70 response: User71 72 - method: PUT73 path: /users/:id74 description: Update user75 body: UpdateUserDto76 response: User77 78 - method: DELETE79 path: /users/:id80 description: Delete user81 response: void82 ```83843. **Define DTOs (Data Transfer Objects)**85 ```typescript86 // CreateUserDto87 interface CreateUserDto {88 email: string; // required, email format89 password: string; // required, min 8 chars90 name: string; // required, min 2 chars91 role?: UserRole; // optional, default: 'user'92 }93 94 // UpdateUserDto95 type UpdateUserDto = Partial<CreateUserDto>;96 97 // UserResponseDto98 interface UserResponseDto {99 id: string;100 email: string;101 name: string;102 role: UserRole;103 createdAt: DateTime;104 updatedAt: DateTime;105 // Note: password NOT included106 }107 ```1081094. **Plan Error Responses**110 ```yaml111 errors:112 - code: 400113 when: Invalid input114 response: { message, errors: [{field, message}] }115 116 - code: 401117 when: Not authenticated118 response: { message: "Unauthorized" }119 120 - code: 403121 when: Not authorized122 response: { message: "Forbidden" }123 124 - code: 404125 when: Resource not found126 response: { message: "User not found" }127 128 - code: 409129 when: Conflict (e.g., email exists)130 response: { message: "Email already registered" }131 ```132133**Output:** Complete API design document.134135---136137### Phase 2: STRUCTURE 🏗️138139**Goal:** Set up the file structure.140141**NestJS Structure:**142```143src/144└── users/145 ├── users.module.ts # Module definition146 ├── users.controller.ts # HTTP layer147 ├── users.service.ts # Business logic148 ├── users.repository.ts # Data access (optional)149 ├── dto/150 │ ├── create-user.dto.ts151 │ ├── update-user.dto.ts152 │ └── user-response.dto.ts153 ├── entities/154 │ └── user.entity.ts155 ├── guards/156 │ └── user-owner.guard.ts157 └── users.controller.spec.ts158```159160**FastAPI Structure:**161```162app/163└── users/164 ├── __init__.py165 ├── router.py # Routes166 ├── service.py # Business logic167 ├── repository.py # Data access168 ├── schemas.py # Pydantic models169 ├── models.py # SQLAlchemy models170 └── dependencies.py # Dependency injection171```172173---174175### Phase 3: IMPLEMENTATION 💻176177**Goal:** Implement the API layer by layer.178179**Order of Implementation:**1801811. **Entity/Model First**182 ```typescript183 // user.entity.ts184 @Entity('users')185 export class User {186 @PrimaryGeneratedColumn('uuid')187 id: string;188 189 @Column({ unique: true })190 @IsEmail()191 email: string;192 193 @Column()194 @Exclude() // Never expose password195 password: string;196 197 @Column()198 name: string;199 200 @Column({ default: 'user' })201 role: UserRole;202 203 @CreateDateColumn()204 createdAt: Date;205 206 @UpdateDateColumn()207 updatedAt: Date;208 }209 ```2102112. **DTOs with Validation**212 ```typescript213 // create-user.dto.ts214 export class CreateUserDto {215 @IsEmail()216 @Transform(({ value }) => value.toLowerCase().trim())217 email: string;218 219 @IsString()220 @MinLength(8)221 @Matches(/^(?=.*[A-Za-z])(?=.*\d)/, {222 message: 'Password must contain letters and numbers'223 })224 password: string;225 226 @IsString()227 @MinLength(2)228 @MaxLength(50)229 name: string;230 231 @IsOptional()232 @IsEnum(UserRole)233 role?: UserRole;234 }235 ```2362373. **Service Layer (Business Logic)**238 ```typescript239 // users.service.ts240 @Injectable()241 export class UsersService {242 constructor(243 @InjectRepository(User)244 private usersRepository: Repository<User>,245 ) {}246 247 async create(dto: CreateUserDto): Promise<User> {248 // Check for existing email249 const existing = await this.findByEmail(dto.email);250 if (existing) {251 throw new ConflictException('Email already registered');252 }253 254 // Hash password255 const hashedPassword = await bcrypt.hash(dto.password, 10);256 257 // Create and save258 const user = this.usersRepository.create({259 ...dto,260 password: hashedPassword,261 });262 263 return this.usersRepository.save(user);264 }265 266 async findAll(options: PaginationOptions): Promise<PaginatedResult<User>> {267 // Implementation with pagination268 }269 270 // ... other methods271 }272 ```2732744. **Controller (HTTP Layer)**275 ```typescript276 // users.controller.ts277 @Controller('users')278 @UseInterceptors(ClassSerializerInterceptor)279 export class UsersController {280 constructor(private readonly usersService: UsersService) {}281 282 @Post()283 @HttpCode(HttpStatus.CREATED)284 async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {285 const user = await this.usersService.create(dto);286 return plainToInstance(UserResponseDto, user);287 }288 289 @Get()290 @UseGuards(AuthGuard)291 async findAll(292 @Query() query: PaginationQueryDto293 ): Promise<PaginatedResult<UserResponseDto>> {294 return this.usersService.findAll(query);295 }296 297 @Get(':id')298 @UseGuards(AuthGuard)299 async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<UserResponseDto> {300 const user = await this.usersService.findOne(id);301 if (!user) {302 throw new NotFoundException('User not found');303 }304 return plainToInstance(UserResponseDto, user);305 }306 307 // ... other endpoints308 }309 ```310311---312313### Phase 4: SECURITY 🔒314315**Goal:** Ensure API is secure.316317**Security Checklist:**3183191. **Input Validation**320 - [ ] All inputs validated with DTOs321 - [ ] Types enforced322 - [ ] Length limits set323 - [ ] Format validation (email, UUID, etc.)3243252. **Authentication**326 - [ ] Protected routes require authentication327 - [ ] JWT or session validation328 - [ ] Token expiration handled3293303. **Authorization**331 - [ ] Role-based access control332 - [ ] Resource ownership verified333 - [ ] Admin-only routes protected3343354. **Data Protection**336 - [ ] Passwords hashed (bcrypt, argon2)337 - [ ] Sensitive data not logged338 - [ ] Passwords excluded from responses3393405. **Rate Limiting**341 - [ ] Login attempts limited342 - [ ] API rate limiting in place3433446. **SQL Injection Prevention**345 - [ ] Parameterized queries used346 - [ ] ORM used correctly347 - [ ] Raw queries avoided or sanitized348349---350351### Phase 5: TESTING 🧪352353**Goal:** Write comprehensive tests.354355**Test Types:**3563571. **Unit Tests**358 ```typescript359 describe('UsersService', () => {360 describe('create', () => {361 it('should create a new user', async () => {362 const dto = { email: 'test@example.com', ... };363 const result = await service.create(dto);364 expect(result.email).toBe(dto.email);365 });366 367 it('should hash the password', async () => {368 const dto = { password: 'plaintext', ... };369 const result = await service.create(dto);370 expect(result.password).not.toBe(dto.password);371 });372 373 it('should throw on duplicate email', async () => {374 // Setup: create user first375 await service.create({ email: 'test@example.com', ... });376 377 // Act & Assert378 await expect(379 service.create({ email: 'test@example.com', ... })380 ).rejects.toThrow(ConflictException);381 });382 });383 });384 ```3853862. **Integration Tests**387 ```typescript388 describe('Users API', () => {389 it('POST /users should create user', async () => {390 const response = await request(app.getHttpServer())391 .post('/users')392 .send({ email: 'test@example.com', password: 'Password1', name: 'Test' })393 .expect(201);394 395 expect(response.body.email).toBe('test@example.com');396 expect(response.body.password).toBeUndefined();397 });398 399 it('GET /users should require auth', async () => {400 await request(app.getHttpServer())401 .get('/users')402 .expect(401);403 });404 });405 ```406407---408409### Phase 6: DOCUMENTATION 📝410411**Goal:** Document the API.412413**OpenAPI/Swagger:**414```typescript415@ApiTags('users')416@Controller('users')417export class UsersController {418 @Post()419 @ApiOperation({ summary: 'Create a new user' })420 @ApiResponse({ status: 201, type: UserResponseDto })421 @ApiResponse({ status: 400, description: 'Invalid input' })422 @ApiResponse({ status: 409, description: 'Email already exists' })423 async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {424 // ...425 }426}427```428429**Response DTO Documentation:**430```typescript431export class UserResponseDto {432 @ApiProperty({ example: '550e8400-e29b-41d4-a716-446655440000' })433 id: string;434 435 @ApiProperty({ example: 'user@example.com' })436 email: string;437 438 @ApiProperty({ example: 'John Doe' })439 name: string;440}441```442443---444445## Best Practices446447### API Design448449| Practice | Do | Don't |450|----------|----|----- |451| Naming | `GET /users/:id/orders` | `GET /getUserOrders` |452| Versioning | `/api/v1/users` | No versioning |453| Pluralization | `/users`, `/orders` | `/user`, `/order` |454| HTTP Methods | Use correctly (GET=read, POST=create) | POST for everything |455| Status Codes | 201 for created, 204 for no content | 200 for everything |456457### Error Handling458459```typescript460// Global exception filter461@Catch()462export class AllExceptionsFilter implements ExceptionFilter {463 catch(exception: unknown, host: ArgumentsHost) {464 const ctx = host.switchToHttp();465 const response = ctx.getResponse<Response>();466 467 const status = exception instanceof HttpException468 ? exception.getStatus()469 : HttpStatus.INTERNAL_SERVER_ERROR;470 471 const message = exception instanceof HttpException472 ? exception.message473 : 'Internal server error';474 475 response.status(status).json({476 statusCode: status,477 message,478 timestamp: new Date().toISOString(),479 });480 }481}482```483484---485486## Guidelines487488### DO ✅489- Design API before coding490- Use proper HTTP methods and status codes491- Validate all inputs492- Handle errors gracefully493- Write tests first (TDD)494- Document with OpenAPI495496### DON'T ❌497- Expose internal IDs when UUIDs are better498- Return password or sensitive data499- Use GET for mutations500- Skip input validation501- Catch and swallow errors502- Use magic strings/numbers503504---505506## Success Criteria507508Before considering API complete:509510- [ ] All endpoints implemented per design511- [ ] Input validation on all endpoints512- [ ] Authentication/Authorization in place513- [ ] Error handling comprehensive514- [ ] Unit tests with >80% coverage515- [ ] Integration tests for main flows516- [ ] API documented (OpenAPI/Swagger)517- [ ] Security checklist passed518519---520521## Related Skills522523- `skills/kilo-kit/development/database/` - For data layer524- `skills/kilo-kit/development/security/` - For security concerns525- `skills/kilo-kit/quality/testing/` - For test coverage526- `skills/kilo-kit/architecture/system-design/` - For architecture decisions527528---529530*Backend API Development Skill v1.0.0 — APIs built right*