REST API Design Skill
Purpose
Design RESTful APIs following industry best practices.
HTTP Methods
| Method | Action | Idempotent | Safe | Request Body | Response Body | |--------|--------|------------|------|--------------|---------------| | GET | Read | Yes | Yes | No | Yes | | POST | Create | No | No | Yes | Yes | | PUT | Replace | Yes | No | Yes | Yes | | PATCH | Update | No | No | Yes | Yes | | DELETE | Delete | Yes | No | No | No | | HEAD | Metadata | Yes | Yes | No | No | | OPTIONS | Capabilities | Yes | Yes | No | Yes |
Status Codes
Success (2xx)
200 OK → GET, PUT, PATCH success
201 Created → POST success (include Location header)
202 Accepted → Async operation started
204 No Content → DELETE success
Client Errors (4xx)
400 Bad Request → Validation failed
401 Unauthorized → Authentication required
403 Forbidden → Permission denied
404 Not Found → Resource doesn't exist
409 Conflict → State conflict (duplicate, etc.)
422 Unprocessable → Semantic errors
429 Too Many → Rate limit exceeded
Server Errors (5xx)
500 Internal → Unexpected error
502 Bad Gateway → Upstream failed
503 Unavailable → Temporarily down
504 Timeout → Upstream timeout
Resource Design
# Collection operations
GET /api/v1/users → List (paginated)
POST /api/v1/users → Create
# Instance operations
GET /api/v1/users/{id} → Read
PUT /api/v1/users/{id} → Replace
PATCH /api/v1/users/{id} → Update
DELETE /api/v1/users/{id} → Delete
# Nested resources (max 2 levels)
GET /api/v1/users/{id}/orders → User's orders
POST /api/v1/users/{id}/orders → Create user's order
# Sub-resource instance
GET /api/v1/users/{id}/orders/{orderId}
# Actions (when CRUD doesn't fit)
POST /api/v1/orders/{id}/cancel
POST /api/v1/users/{id}/verify
POST /api/v1/reports/{id}/generate
Response Envelope
{
"data": {
"id": "123",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com"
}
},
"meta": {
"requestId": "abc-123",
"timestamp": "2024-12-30T10:00:00Z",
"version": "1.0.0"
}
}
Error Response (RFC 7807)
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Failed",
"status": 400,
"detail": "One or more fields have validation errors",
"instance": "/api/v1/users",
"errors": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "password", "message": "Minimum 12 characters" }
]
}
Pagination
Offset-based (Simple)
GET /api/v1/users?page=2&limit=20
Response:
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
Cursor-based (Recommended)
GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIwfQ",
"prevCursor": "eyJpZCI6ODB9",
"hasNext": true,
"hasPrev": true
}
}
Filtering & Sorting
# Filtering
GET /api/v1/users?status=active&role=admin
GET /api/v1/users?created_at[gte]=2024-01-01
GET /api/v1/users?search=john
# Sorting
GET /api/v1/users?sort=created_at:desc
GET /api/v1/users?sort=-created_at,name # - prefix for desc
# Field selection
GET /api/v1/users?fields=id,name,email
# Embedding related resources
GET /api/v1/users?include=orders,profile
Caching Headers
# Response headers
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
Vary: Accept-Encoding, Authorization
# Conditional requests
If-None-Match: "abc123"
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
Unit Test Template
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import app from './app';
describe('REST API - Users', () => {
describe('GET /api/v1/users', () => {
it('should return paginated users', async () => {
const res = await request(app)
.get('/api/v1/users?page=1&limit=10')
.expect(200);
expect(res.body).toHaveProperty('data');
expect(res.body).toHaveProperty('pagination');
expect(res.body.pagination.page).toBe(1);
});
it('should filter by status', async () => {
const res = await request(app)
.get('/api/v1/users?status=active')
.expect(200);
res.body.data.forEach(user => {
expect(user.status).toBe('active');
});
});
});
describe('POST /api/v1/users', () => {
it('should create user and return 201', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(201);
expect(res.headers.location).toMatch(/\/api\/v1\/users\/\w+/);
expect(res.body.data.id).toBeDefined();
});
it('should return 400 for invalid data', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({ email: 'invalid' })
.expect(400);
expect(res.body.type).toContain('validation');
});
});
describe('GET /api/v1/users/:id', () => {
it('should return 404 for non-existent user', async () => {
await request(app)
.get('/api/v1/users/non-existent-id')
.expect(404);
});
});
});
Troubleshooting
| Issue | Cause | Solution | |-------|-------|----------| | 404 for valid resource | Trailing slash mismatch | Normalize URLs | | CORS errors | Missing headers | Configure CORS middleware | | Slow pagination | Large offset | Use cursor pagination | | Cache not working | Missing Vary header | Add Vary: Authorization |
Quality Checklist
- [ ] Resource names are plural nouns
- [ ] HTTP methods match actions
- [ ] Status codes are correct
- [ ] Error responses follow RFC 7807
- [ ] Pagination implemented for lists
- [ ] Filtering and sorting supported
- [ ] Caching headers configured
- [ ] Rate limiting in place