RESTful API Design Best Practices
Introduction
RESTful API design is crucial for building scalable, maintainable, and intuitive web services. This guide covers HTTP methods, status codes, resource naming, versioning, pagination, filtering, and advanced patterns for professional API development.
1. Resource Naming Conventions
URL Structure Best Practices
✓ GOOD Examples:
GET /api/users # List all users
GET /api/users/123 # Get specific user
GET /api/users/123/posts # User's posts
GET /api/posts?author=123 # Posts filtered by author
POST /api/users # Create user
PUT /api/users/123 # Update entire user
PATCH /api/users/123 # Partial update
DELETE /api/users/123 # Delete user
✗ BAD Examples:
GET /api/getUsers # Verb in URL
GET /api/user/123 # Singular noun
POST /api/users/create # Redundant verb
GET /api/users/123/getPosts # Verb in URL
DELETE /api/deleteUser?id=123 # Verb + query param
Guidelines:
- Use plural nouns for resources
- Use nouns, not verbs
- Use lowercase with hyphens for multi-word
- Nested resources for relationships
- Maximum 2-3 levels of nesting
2. HTTP Methods
Standard HTTP Verbs
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource(s) | Yes | Yes |
| POST | Create new resource | No | No |
| PUT | Replace entire resource | Yes | No |
| PATCH | Partial update | No* | No |
| DELETE | Remove resource | Yes | No |
| HEAD | Get headers only | Yes | Yes |
| OPTIONS | Get allowed methods | Yes | Yes |
Implementation Examples
// Express.js REST endpoints
import express from 'express';
const router = express.Router();
// GET - Retrieve all
router.get('/users', async (req, res) => {
const users = await User.find();
res.json({ data: users });
});
// GET - Retrieve one
router.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json({ data: user });
});
// POST - Create
router.post('/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json({ data: user });
});
// PUT - Full replacement
router.put('/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, overwrite: true }
);
res.json({ data: user });
});
// PATCH - Partial update
router.patch('/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true }
);
res.json({ data: user });
});
// DELETE
router.delete('/users/:id', async (req, res) => {
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
});
3. HTTP Status Codes
Common Status Codes
2xx Success:
200 OK - Successful GET, PUT, PATCH
201 Created - Successful POST
204 No Content - Successful DELETE
206 Partial Content - Pagination/chunked response
3xx Redirection:
301 Moved Permanently - Resource URL changed
304 Not Modified - Cached version valid
4xx Client Errors:
400 Bad Request - Invalid syntax/validation error
401 Unauthorized - Authentication required
403 Forbidden - No permission
404 Not Found - Resource doesn't exist
405 Method Not Allowed - Wrong HTTP method
409 Conflict - Resource conflict (duplicate)
422 Unprocessable - Semantic errors
429 Too Many Requests - Rate limit exceeded
5xx Server Errors:
500 Internal Server Error - Generic server error
502 Bad Gateway - Upstream server error
503 Service Unavailable - Temporary unavailable
504 Gateway Timeout - Upstream timeout
Error Response Format
// Consistent error response structure
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Email is required"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
],
"timestamp": "2025-01-15T10:30:00Z",
"path": "/api/users",
"requestId": "abc-123-def-456"
}
}
4. Pagination
Pagination Strategies
// 1. Offset-based pagination
GET /api/users?page=2&limit=20
// Implementation
router.get('/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
const [users, total] = await Promise.all([
User.find().skip(skip).limit(limit),
User.countDocuments()
]);
res.json({
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
});
});
// 2. Cursor-based pagination (better for large datasets)
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
router.get('/users', async (req, res) => {
const limit = parseInt(req.query.limit) || 20;
const cursor = req.query.cursor
? JSON.parse(Buffer.from(req.query.cursor, 'base64').toString())
: null;
const query = cursor ? { _id: { $gt: cursor.id } } : {};
const users = await User.find(query).limit(limit + 1);
const hasMore = users.length > limit;
const items = hasMore ? users.slice(0, -1) : users;
const nextCursor = hasMore
? Buffer.from(JSON.stringify({ id: items[items.length - 1]._id })).toString('base64')
: null;
res.json({
data: items,
pagination: {
nextCursor,
hasMore
}
});
});
5. Filtering, Sorting & Searching
// Filtering
GET /api/products?category=electronics&price[gte]=100&price[lte]=500
// Sorting
GET /api/products?sort=-price,name // -price desc, name asc
// Searching
GET /api/products?search=laptop&fields=name,description
// Field selection
GET /api/users?fields=name,email,-password
// Implementation
router.get('/products', async (req, res) => {
// Build filter
const filter = {};
if (req.query.category) filter.category = req.query.category;
if (req.query['price[gte]']) filter.price = { $gte: req.query['price[gte]'] };
if (req.query['price[lte]']) filter.price = { ...filter.price, $lte: req.query['price[lte]'] };
// Build sort
const sort = {};
if (req.query.sort) {
req.query.sort.split(',').forEach(field => {
if (field.startsWith('-')) {
sort[field.slice(1)] = -1;
} else {
sort[field] = 1;
}
});
}
// Search
if (req.query.search) {
filter.$text = { $search: req.query.search };
}
// Field selection
let select = '';
if (req.query.fields) {
select = req.query.fields.replace(/,/g, ' ');
}
const products = await Product.find(filter)
.sort(sort)
.select(select)
.limit(20);
res.json({ data: products });
});
6. API Versioning
Versioning Strategies
// 1. URL Versioning (Recommended)
GET /api/v1/users
GET /api/v2/users
// 2. Header Versioning
GET /api/users
Accept: application/vnd.myapi.v2+json
// 3. Query Parameter
GET /api/users?version=2
// Express implementation - URL versioning
import express from 'express';
// V1 routes
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
res.json({ version: 'v1', users: [] });
});
// V2 routes
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
res.json({ version: 'v2', users: [], newField: true });
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Deprecation header
app.use('/api/v1', (req, res, next) => {
res.set('X-API-Warn', 'API v1 is deprecated. Please migrate to v2.');
next();
});
7. HATEOAS (Hypermedia)
// Include links to related resources
{
"data": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
},
"links": {
"self": "/api/users/123",
"posts": "/api/users/123/posts",
"followers": "/api/users/123/followers",
"following": "/api/users/123/following"
},
"actions": [
{
"name": "update",
"method": "PATCH",
"href": "/api/users/123"
},
{
"name": "delete",
"method": "DELETE",
"href": "/api/users/123"
}
]
}
8. Rate Limiting Headers
// Standard rate limit headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 75
X-RateLimit-Reset: 1642521600
// Implementation
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
handler: (req, res) => {
res.status(429).json({
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests, please try again later',
retryAfter: req.rateLimit.resetTime
}
});
}
});
9. Content Negotiation
// Support multiple response formats
GET /api/users
Accept: application/json # JSON response
Accept: application/xml # XML response
Accept: text/csv # CSV response
// Express implementation
router.get('/users', async (req, res) => {
const users = await User.find();
res.format({
'application/json': () => {
res.json({ data: users });
},
'application/xml': () => {
const xml = `
${users.map(u => `${u.id} `).join('')} `;
res.send(xml);
},
'text/csv': () => {
const csv = users.map(u => `${u.id},${u.name},${u.email}`).join('\n');
res.send(csv);
},
'default': () => {
res.status(406).send('Not Acceptable');
}
});
});
10. API Documentation
// OpenAPI/Swagger specification
/**
* @swagger
* /api/users:
* get:
* summary: Get all users
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: Page number
* responses:
* 200:
* description: List of users
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/
// Install swagger
npm install swagger-jsdoc swagger-ui-express
// Setup
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
},
},
apis: ['./routes/*.ts'],
};
const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
Best Practices Checklist
✓ RESTful API Design Checklist:
- Use plural nouns for resource names
- Use proper HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Return appropriate status codes
- Implement pagination for list endpoints
- Support filtering, sorting, and searching
- Version your API (URL or header)
- Use consistent error response format
- Implement rate limiting
- Support CORS for web clients
- Use HTTPS in production
- Document API with OpenAPI/Swagger
- Include HATEOAS links when beneficial
- Support content negotiation
- Use ETags for caching
- Implement proper authentication
Conclusion
Well-designed REST APIs are intuitive, consistent, and easy to integrate. By following HTTP standards, using proper status codes, implementing pagination and filtering, and maintaining good documentation, you create APIs that developers love to use and that scale effectively.
💡 Pro Tip: Consistency is more important than perfection. Choose conventions (naming, error formats, pagination style) and stick to them throughout your API. Document deviations and always think about the developer experience.