RESTful API design principles and best practices
Design RESTful APIs following industry best practices.
| 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 |
200 OK โ GET, PUT, PATCH success
201 Created โ POST success (include Location header)
202 Accepted โ Async operation started
204 No Content โ DELETE success
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
500 Internal โ Unexpected error
502 Bad Gateway โ Upstream failed
503 Unavailable โ Temporarily down
504 Timeout โ Upstream timeout
# 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
{
"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"
}
}
{
"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" }
]
}
GET /api/v1/users?page=2&limit=20
Response:
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTIwfQ",
"prevCursor": "eyJpZCI6ODB9",
"hasNext": true,
"hasPrev": true
}
}
# 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
# 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
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);
});
});
});
| 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 |