Create API Endpoint Skill
Purpose
This skill guides you through creating a robust, secure, and well-documented RESTful API endpoint following industry best practices.
When to Use
- •Creating new API endpoints
- •Exposing backend functionality to frontend
- •Building RESTful services
- •Need standardized endpoint structure
Prerequisites
- •Clear understanding of the endpoint's purpose
- •Data models defined (coordinate with Database_Architect)
- •Authentication requirements
- •Technology stack (Express, FastAPI, ASP.NET, etc.)
Step-by-Step Process
1. Define API Contract
yaml
# OpenAPI/Swagger Specification
paths:
/api/users:
post:
summary: Create a new user
tags:
- Users
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- password
- name
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
name:
type: string
responses:
'201':
description: User created successfully
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Validation error
'409':
description: User already exists
'500':
description: Server error
Contract Checklist:
- • HTTP method (GET, POST, PUT, PATCH, DELETE)
- • URL path with parameters
- • Request body schema
- • Response schemas (success and errors)
- • Authentication requirements
- • Rate limiting needs
2. Implement Request Validation
typescript
// Node.js/Express with Zod
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[0-9]/, 'Password must contain number'),
name: z.string().min(1, 'Name is required').max(100),
});
type CreateUserInput = z.infer<typeof createUserSchema>;
python
# Python/FastAPI with Pydantic
from pydantic import BaseModel, EmailStr, Field, validator
class CreateUserInput(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
name: str = Field(..., min_length=1, max_length=100)
@validator('password')
def validate_password(cls, v):
if not any(c.isupper() for c in v):
raise ValueError('Password must contain uppercase letter')
if not any(c.isdigit() for c in v):
raise ValueError('Password must contain number')
return v
Validation Checklist:
- • Required fields validated
- • Data types validated
- • Format validation (email, URL, etc.)
- • Length constraints
- • Pattern matching (regex)
- • Custom business rules
3. Implement Endpoint Logic
typescript
// Example: Node.js/Express
import { Router } from 'express';
import { authenticate } from '../middleware/auth';
import { validateRequest } from '../middleware/validation';
import { UserService } from '../services/user.service';
const router = Router();
router.post(
'/api/users',
authenticate, // Middleware: Check authentication
validateRequest(createUserSchema), // Middleware: Validate request
async (req, res, next) => {
try {
const input: CreateUserInput = req.body;
// Business logic
const userService = new UserService();
const user = await userService.createUser(input);
// Success response
res.status(201).json({
success: true,
data: user,
message: 'User created successfully',
});
} catch (error) {
next(error); // Pass to error handler
}
}
);
export default router;
Implementation Checklist:
- • Use appropriate HTTP status codes
- • Implement proper error handling
- • Call service layer (don't put logic in controller)
- • Return consistent response format
- • Add logging
- • Handle async operations properly
4. Implement Service Layer
typescript
// user.service.ts
import bcrypt from 'bcrypt';
import { UserRepository } from '../repositories/user.repository';
import { ConflictError } from '../errors/ConflictError';
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
async createUser(input: CreateUserInput) {
// Check if user exists
const existingUser = await this.userRepository.findByEmail(input.email);
if (existingUser) {
throw new ConflictError('User with this email already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(input.password, 10);
// Create user
const user = await this.userRepository.create({
email: input.email,
password: hashedPassword,
name: input.name,
});
// Don't return password
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
}
}
Service Layer Checklist:
- • Separate business logic from HTTP concerns
- • Use repository pattern for data access
- • Implement proper error handling
- • Don't leak sensitive data (passwords, tokens)
- • Add transaction support if needed
- • Log important operations
5. Implement Error Handling
typescript
// errors/ConflictError.ts
export class ConflictError extends Error {
statusCode = 409;
constructor(message: string) {
super(message);
this.name = 'ConflictError';
}
}
// middleware/errorHandler.ts
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
// Log error
console.error('Error:', {
name: err.name,
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
});
// Determine status code
const statusCode = (err as any).statusCode || 500;
// Send error response
res.status(statusCode).json({
success: false,
error: {
type: err.name,
message: statusCode === 500 ? 'Internal server error' : err.message,
},
});
}
Error Handling Checklist:
- • Custom error classes for different scenarios
- • Appropriate HTTP status codes
- • Don't expose internal errors to clients
- • Log errors with context
- • Consistent error response format
6. Add Authentication & Authorization
typescript
// middleware/auth.ts
import jwt from 'jsonwebtoken';
import { UnauthorizedError } from '../errors/UnauthorizedError';
export async function authenticate(req: Request, res: Response, next: NextFunction) {
try {
// Extract token from header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedError('Missing or invalid authorization header');
}
const token = authHeader.substring(7);
// Verify token
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
// Attach user to request
req.user = payload;
next();
} catch (error) {
next(new UnauthorizedError('Invalid or expired token'));
}
}
// Authorization example
export function requireRole(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return next(new ForbiddenError('Insufficient permissions'));
}
next();
};
}
Auth Checklist:
- • Validate authentication token
- • Check user permissions/roles
- • Handle expired tokens
- • Protect sensitive endpoints
- • Use secure token storage
7. Add Rate Limiting
typescript
import rateLimit from 'express-rate-limit';
const createUserLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Max 5 requests per window
message: 'Too many accounts created, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
router.post('/api/users', createUserLimiter, ...);
Rate Limiting Checklist:
- • Protect against brute force
- • Set appropriate limits
- • Provide clear error messages
- • Consider different limits for authenticated users
8. Write Tests
typescript
// user.controller.test.ts
import request from 'supertest';
import { app } from '../app';
describe('POST /api/users', () => {
it('creates a new user successfully', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'test@example.com',
password: 'Password123',
name: 'Test User',
});
expect(response.status).toBe(201);
expect(response.body.success).toBe(true);
expect(response.body.data.email).toBe('test@example.com');
expect(response.body.data.password).toBeUndefined(); // Password should not be returned
});
it('returns 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({
email: 'invalid-email',
password: 'Password123',
name: 'Test User',
});
expect(response.status).toBe(400);
});
it('returns 409 for duplicate email', async () => {
// Create first user
await request(app).post('/api/users').send({
email: 'duplicate@example.com',
password: 'Password123',
name: 'First User',
});
// Try to create duplicate
const response = await request(app)
.post('/api/users')
.send({
email: 'duplicate@example.com',
password: 'Password456',
name: 'Second User',
});
expect(response.status).toBe(409);
});
it('requires authentication', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', password: 'Password123', name: 'Test' });
expect(response.status).toBe(401);
});
});
Testing Checklist:
- • Test successful cases
- • Test validation errors
- • Test business logic errors
- • Test authentication/authorization
- • Test edge cases
- • Test error handling
9. Document the Endpoint
markdown
# Create User Endpoint
## POST /api/users
Creates a new user account.
### Authentication
Requires bearer token authentication.
### Request Body
\`\`\`json
{
"email": "user@example.com",
"password": "Password123",
"name": "John Doe"
}
\`\`\`
### Responses
#### 201 Created
\`\`\`json
{
"success": true,
"data": {
"id": "123",
"email": "user@example.com",
"name": "John Doe",
"createdAt": "2024-01-01T00:00:00Z"
},
"message": "User created successfully"
}
\`\`\`
#### 400 Bad Request
Invalid input data.
#### 409 Conflict
User with this email already exists.
#### 500 Internal Server Error
Server error occurred.
### Example
\`\`\`bash
curl -X POST https://api.example.com/api/users \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer YOUR_TOKEN" \\
-d '{
"email": "user@example.com",
"password": "Password123",
"name": "John Doe"
}'
\`\`\`
HTTP Status Codes Reference
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST (resource created) |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Validation error |
| 401 | Unauthorized | Missing/invalid authentication |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource conflict (duplicate) |
| 422 | Unprocessable Entity | Semantic validation error |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
Response Format Standard
typescript
// Success response
{
success: true,
data: { /* response data */ },
message?: string, // Optional success message
}
// Error response
{
success: false,
error: {
type: string, // Error type/code
message: string, // Human-readable message
details?: any, // Additional error details
}
}
Best Practices
- •RESTful Design: Use proper HTTP methods and status codes
- •Validation First: Validate all input before processing
- •Security: Always authenticate/authorize protected endpoints
- •Error Handling: Handle all errors gracefully
- •Logging: Log important operations and errors
- •Documentation: Document all endpoints thoroughly
- •Testing: Test success and failure scenarios
- •Performance: Consider caching and pagination
Security Checklist
- • Input validation and sanitization
- • Authentication required for protected endpoints
- • Authorization checks for role-based access
- • Rate limiting to prevent abuse
- • SQL injection prevention (use parameterized queries)
- • XSS prevention (sanitize output)
- • CSRF protection
- • Secure password hashing (bcrypt, argon2)
- • No sensitive data in logs or responses
- • HTTPS only in production
Common Pitfalls to Avoid
❌ Don't put business logic in controllers
❌ Don't return sensitive data (passwords, tokens)
❌ Don't use string concatenation for SQL queries
❌ Don't ignore error handling
❌ Don't expose internal error details to clients
❌ Don't skip input validation
Related Skills
- •
validate_auth_flow- Validate authentication implementation - •
review_security- Security review for endpoints - •
design_database_schema- If endpoint needs new data models