HTML & JS Topic
Node.js RESTful APIs
What is a REST API?
REST (Representational State Transfer) is an architectural style for designing networked applications. A RESTful API is an API that follows REST principles, using HTTP protocols for communication.
Key Characteristics:
-
Client-Server Architecture: Separation of concerns
-
Stateless: Each request contains all necessary information
-
Cacheable: Responses can be cached
-
Uniform Interface: Consistent way of interacting
-
Layered System: Can have intermediaries (proxies, load balancers)
-
Code on Demand (optional): Servers can send executable code
REST vs SOAP vs GraphQL:
javascript
// REST (Resource-oriented)
GET /api/users/123
POST /api/users
PUT /api/users/123
DELETE /api/users/123
// SOAP (Action-oriented - XML-based)
POST /soap-endpoint
Body: <soap:Envelope>
<soap:Body>
<getUser><id>123</id></getUser>
</soap:Body>
</soap:Envelope>
// GraphQL (Query language)
POST /graphql
Body: { query: "{ user(id: 123) { id name email } }" }
REST API Components:
javascript
// URL Structure
https://api.example.com/v1/users/123/posts?limit=10&page=1
│ │ │ │ │ │ └─────└── Query parameters
│ │ │ │ │ └── Resource ID
│ │ │ │ └── Sub-resource
│ │ │ └── Resource collection
│ │ └── API version
│ └── Domain
└── Protocol
// Example API endpoints
GET /api/v1/users // List all users
GET /api/v1/users/:id // Get specific user
POST /api/v1/users // Create new user
PUT /api/v1/users/:id // Update entire user
PATCH /api/v1/users/:id // Partially update user
DELETE /api/v1/users/:id // Delete user
GET /api/v1/users/:id/posts // Get user's posts
REST Principles
1. Resource-Based Architecture
Everything is a resource identified by URLs.
javascript
// Resources as nouns (not verbs)
// GOOD - Resource-oriented
GET /articles // Get all articles
GET /articles/123 // Get article 123
POST /articles // Create article
PUT /articles/123 // Update article 123
DELETE /articles/123 // Delete article 123
// BAD - Action-oriented (RPC style)
GET /getAllArticles
GET /getArticle?id=123
POST /createArticle
POST /updateArticle
POST /deleteArticle
2. Statelessness
Each request must contain all information needed to process it.
javascript
// Stateless - Session data in each request
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
const token = generateToken(username);
res.json({ token }); // Client stores token
});
// Subsequent requests include token
app.get('/api/profile', authenticateToken, (req, res) => {
// Token verified from Authorization header
res.json({ user: req.user });
});
// Stateful (not RESTful) - Server stores session
app.post('/login', (req, res) => {
req.session.userId = 123; // Server stores state
res.json({ success: true });
});
app.get('/profile', (req, res) => {
const userId = req.session.userId; // Relies on server state
// Not RESTful!
});
3. Uniform Interface
Consistent way to interact with resources.
javascript
// HTTP methods map to CRUD operations
const operations = {
// Collection operations
'GET /users': 'Retrieve all users',
'POST /users': 'Create a new user',
// Single resource operations
'GET /users/:id': 'Retrieve user by ID',
'PUT /users/:id': 'Replace user by ID',
'PATCH /users/:id': 'Partially update user by ID',
'DELETE /users/:id': 'Delete user by ID',
// Sub-resource operations
'GET /users/:id/posts': 'Retrieve user\'s posts',
'POST /users/:id/posts': 'Create post for user',
// Special operations (rare, usually avoid)
'POST /users/:id/activate': 'Custom action (not RESTful)'
};
// Resource representation (usually JSON)
const userRepresentation = {
id: 123,
name: "John Doe",
email: "john@example.com",
_links: { // HATEOAS - optional
self: { href: "/api/v1/users/123" },
posts: { href: "/api/v1/users/123/posts" }
}
};
4. HATEOAS (Hypermedia As The Engine Of Application State)
Resources include links to related resources.
javascript
// API response with HATEOAS
app.get('/api/v1/users/:id', (req, res) => {
const user = getUserById(req.params.id);
res.json({
...user,
_links: {
self: { href: `/api/v1/users/${user.id}`, method: 'GET' },
update: { href: `/api/v1/users/${user.id}`, method: 'PUT' },
delete: { href: `/api/v1/users/${user.id}`, method: 'DELETE' },
posts: { href: `/api/v1/users/${user.id}/posts`, method: 'GET' }
}
});
});
// Collection response with pagination links
app.get('/api/v1/users', (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const { users, total } = getPaginatedUsers(page, limit);
res.json({
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
},
_links: {
self: { href: `/api/v1/users?page=${page}&limit=${limit}` },
first: { href: `/api/v1/users?page=1&limit=${limit}` },
prev: page > 1 ? { href: `/api/v1/users?page=${page-1}&limit=${limit}` } : null,
next: page < Math.ceil(total / limit) ?
{ href: `/api/v1/users?page=${page+1}&limit=${limit}` } : null,
last: { href: `/api/v1/users?page=${Math.ceil(total / limit)}&limit=${limit}` }
}
});
});
5. Cacheability
Responses should indicate if they can be cached.
javascript
// Cache control headers
app.get('/api/v1/products', (req, res) => {
const products = getAllProducts();
// Cache for 1 hour (public cache)
res.set('Cache-Control', 'public, max-age=3600');
res.set('ETag', generateETag(products));
res.json(products);
});
// Conditional requests (ETag/Last-Modified)
app.get('/api/v1/products/:id', (req, res) => {
const product = getProduct(req.params.id);
const etag = generateETag(product);
// Check If-None-Match header
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // Not Modified
}
res.set('ETag', etag);
res.set('Last-Modified', product.updatedAt.toUTCString());
res.json(product);
});
// For frequently changing data
app.get('/api/v1/stock/:id', (req, res) => {
res.set('Cache-Control', 'no-cache'); // Revalidate each time
// or 'no-store' for sensitive data
res.json(getStockInfo(req.params.id));
});
CRUD Operations
Complete RESTful CRUD Implementation:
javascript
// models/User.js
class UserModel {
constructor() {
this.users = [
{ id: 1, name: 'Alice', email: 'alice@example.com', active: true },
{ id: 2, name: 'Bob', email: 'bob@example.com', active: true }
];
this.nextId = 3;
}
getAll(query = {}) {
let filtered = [...this.users];
// Filtering
if (query.active) {
filtered = filtered.filter(u => u.active === (query.active === 'true'));
}
if (query.name) {
filtered = filtered.filter(u =>
u.name.toLowerCase().includes(query.name.toLowerCase())
);
}
// Sorting
if (query.sort) {
const [field, order] = query.sort.split(':');
filtered.sort((a, b) => {
const aVal = a[field];
const bVal = b[field];
const dir = order === 'desc' ? -1 : 1;
return aVal > bVal ? dir : aVal < bVal ? -dir : 0;
});
}
// Pagination
const page = parseInt(query.page) || 1;
const limit = parseInt(query.limit) || 10;
const start = (page - 1) * limit;
const end = start + limit;
return {
data: filtered.slice(start, end),
total: filtered.length,
page,
limit,
pages: Math.ceil(filtered.length / limit)
};
}
getById(id) {
return this.users.find(u => u.id === parseInt(id));
}
create(userData) {
const newUser = {
id: this.nextId++,
...userData,
createdAt: new Date(),
updatedAt: new Date()
};
this.users.push(newUser);
return newUser;
}
update(id, userData) {
const index = this.users.findIndex(u => u.id === parseInt(id));
if (index === -1) return null;
this.users[index] = {
...this.users[index],
...userData,
updatedAt: new Date()
};
return this.users[index];
}
partialUpdate(id, userData) {
const index = this.users.findIndex(u => u.id === parseInt(id));
if (index === -1) return null;
// Only update provided fields
Object.keys(userData).forEach(key => {
if (userData[key] !== undefined) {
this.users[index][key] = userData[key];
}
});
this.users[index].updatedAt = new Date();
return this.users[index];
}
delete(id) {
const index = this.users.findIndex(u => u.id === parseInt(id));
if (index === -1) return null;
const deleted = this.users.splice(index, 1);
return deleted[0];
}
}
module.exports = new UserModel();
Controllers:
javascript
// controllers/userController.js
const User = require('../models/User');
const { validationResult } = require('express-validator');
// Helper function for responses
const sendResponse = (res, statusCode, data = null, message = '') => {
const response = {
success: statusCode >= 200 && statusCode < 300,
message,
data,
timestamp: new Date().toISOString()
};
// Remove null/undefined fields
Object.keys(response).forEach(key => {
if (response[key] === null || response[key] === undefined) {
delete response[key];
}
});
res.status(statusCode).json(response);
};
// GET /api/v1/users - List all users
exports.getAllUsers = async (req, res) => {
try {
const result = User.getAll(req.query);
// Add HATEOAS links
const baseUrl = `${req.protocol}://${req.get('host')}/api/v1/users`;
const links = {
self: `${baseUrl}?page=${result.page}&limit=${result.limit}`,
first: `${baseUrl}?page=1&limit=${result.limit}`,
last: `${baseUrl}?page=${result.pages}&limit=${result.limit}`
};
if (result.page > 1) {
links.prev = `${baseUrl}?page=${result.page - 1}&limit=${result.limit}`;
}
if (result.page < result.pages) {
links.next = `${baseUrl}?page=${result.page + 1}&limit=${result.limit}`;
}
sendResponse(res, 200, {
users: result.data,
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
pages: result.pages
},
_links: links
});
} catch (error) {
sendResponse(res, 500, null, 'Server error');
}
};
// GET /api/v1/users/:id - Get user by ID
exports.getUserById = async (req, res) => {
try {
const user = User.getById(req.params.id);
if (!user) {
return sendResponse(res, 404, null, 'User not found');
}
// Add self link
const userWithLinks = {
...user,
_links: {
self: `${req.protocol}://${req.get('host')}/api/v1/users/${user.id}`,
update: `${req.protocol}://${req.get('host')}/api/v1/users/${user.id}`,
delete: `${req.protocol}://${req.get('host')}/api/v1/users/${user.id}`
}
};
sendResponse(res, 200, userWithLinks);
} catch (error) {
sendResponse(res, 500, null, 'Server error');
}
};
// POST /api/v1/users - Create new user
exports.createUser = async (req, res) => {
try {
// Validate input
const errors = validationResult(req);
if (!errors.isEmpty()) {
return sendResponse(res, 400, { errors: errors.array() }, 'Validation failed');
}
const { name, email } = req.body;
// Check for duplicate email
const existingUser = User.getAll().data.find(u => u.email === email);
if (existingUser) {
return sendResponse(res, 409, null, 'Email already exists');
}
const newUser = User.create({ name, email });
// Set Location header
res.set('Location', `${req.protocol}://${req.get('host')}/api/v1/users/${newUser.id}`);
sendResponse(res, 201, newUser, 'User created successfully');
} catch (error) {
sendResponse(res, 500, null, 'Server error');
}
};
// PUT /api/v1/users/:id - Update entire user
exports.updateUser = async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return sendResponse(res, 400, { errors: errors.array() }, 'Validation failed');
}
const { id } = req.params;
const { name, email, active } = req.body;
// Check if user exists
const existingUser = User.getById(id);
if (!existingUser) {
return sendResponse(res, 404, null, 'User not found');
}
// Update user
const updatedUser = User.update(id, { name, email, active });
sendResponse(res, 200, updatedUser, 'User updated successfully');
} catch (error) {
sendResponse(res, 500, null, 'Server error');
}
};
// PATCH /api/v1/users/:id - Partially update user
exports.partialUpdateUser = async (req, res) => {
try {
const { id } = req.params;
// Check if user exists
const existingUser = User.getById(id);
if (!existingUser) {
return sendResponse(res, 404, null, 'User not found');
}
// Validate at least one field is provided
const updateFields = Object.keys(req.body);
if (updateFields.length === 0) {
return sendResponse(res, 400, null, 'No fields to update');
}
// Validate each field
const errors = [];
if (req.body.email && !isValidEmail(req.body.email)) {
errors.push({ field: 'email', message: 'Invalid email format' });
}
if (errors.length > 0) {
return sendResponse(res, 400, { errors }, 'Validation failed');
}
// Partial update
const updatedUser = User.partialUpdate(id, req.body);
sendResponse(res, 200, updatedUser, 'User partially updated');
} catch (error) {
sendResponse(res, 500, null, 'Server error');
}
};
// DELETE /api/v1/users/:id - Delete user
exports.deleteUser = async (req, res) => {
try {
const { id } = req.params;
const deletedUser = User.delete(id);
if (!deletedUser) {
return sendResponse(res, 404, null, 'User not found');
}
sendResponse(res, 200, null, 'User deleted successfully');
} catch (error) {
sendResponse(res, 500, null, 'Server error');
}
};
// Helper function
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
Routes:
javascript
// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { body, param, query } = require('express-validator');
const userController = require('../controllers/userController');
// Validation middleware
const validateUser = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Name must be between 2 and 50 characters'),
body('email')
.trim()
.isEmail()
.withMessage('Valid email is required')
.normalizeEmail(),
body('active')
.optional()
.isBoolean()
.withMessage('Active must be a boolean')
];
const validateId = [
param('id')
.isInt({ min: 1 })
.withMessage('ID must be a positive integer')
];
// Routes
router.route('/')
.get(
[
query('page').optional().isInt({ min: 1 }),
query('limit').optional().isInt({ min: 1, max: 100 }),
query('active').optional().isBoolean(),
query('sort').optional().isIn(['name:asc', 'name:desc', 'email:asc', 'email:desc'])
],
userController.getAllUsers
)
.post(validateUser, userController.createUser);
router.route('/:id')
.get(validateId, userController.getUserById)
.put(validateId, validateUser, userController.updateUser)
.patch(validateId, userController.partialUpdateUser)
.delete(validateId, userController.deleteUser);
// Sub-resource routes
router.get('/:id/posts', validateId, async (req, res) => {
// Get user's posts
const posts = []; // Fetch from database
res.json({
data: posts,
_links: {
user: `/api/v1/users/${req.params.id}`
}
});
});
module.exports = router;
Main Application:
javascript
// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const app = express();
// Middleware
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
}));
app.use(compression());
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests, please try again later.'
}
});
app.use('/api/', limiter);
// Request logging middleware
app.use((req, res, next) => {
req.requestId = Date.now().toString(36) + Math.random().toString(36).substr(2);
console.log(`[${req.requestId}] ${req.method} ${req.url}`);
next();
});
// API Routes
app.use('/api/v1/users', require('./routes/userRoutes'));
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// API documentation
app.get('/api-docs', (req, res) => {
res.json({
name: 'User Management API',
version: '1.0.0',
description: 'RESTful API for managing users',
endpoints: {
users: {
getAll: { method: 'GET', path: '/api/v1/users' },
getOne: { method: 'GET', path: '/api/v1/users/:id' },
create: { method: 'POST', path: '/api/v1/users' },
update: { method: 'PUT', path: '/api/v1/users/:id' },
delete: { method: 'DELETE', path: '/api/v1/users/:id' }
}
}
});
});
// 404 handler for API routes
app.use('/api/*', (req, res) => {
res.status(404).json({
error: 'Endpoint not found',
requestId: req.requestId,
path: req.originalUrl
});
});
// Global error handler
app.use((err, req, res, next) => {
console.error(`[${req.requestId}] Error:`, err);
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
error: message,
requestId: req.requestId,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`REST API Server running on port ${PORT}`);
});
module.exports = app;
Status Codes
Informational (1xx):
javascript
// Rarely used directly in REST APIs
res.writeContinue(); // 100 Continue
res.writeProcessing(); // 102 Processing
Success (2xx):
javascript
// 200 OK - General success
app.get('/users', (req, res) => {
res.status(200).json(users);
});
// 201 Created - Resource created
app.post('/users', (req, res) => {
const newUser = createUser(req.body);
res.status(201)
.set('Location', `/users/${newUser.id}`)
.json(newUser);
});
// 202 Accepted - Request accepted for processing
app.post('/webhooks', (req, res) => {
processWebhookAsync(req.body); // Process later
res.status(202).json({ message: 'Request accepted' });
});
// 204 No Content - Success but no response body
app.delete('/users/:id', (req, res) => {
deleteUser(req.params.id);
res.status(204).end();
});
// 205 Reset Content - Clear form/reset UI
app.post('/form-reset', (req, res) => {
// Process data
res.status(205).end(); // Client should reset form
});
// 206 Partial Content - For range requests
app.get('/large-file', (req, res) => {
const range = req.headers.range;
// Send partial content
res.status(206)
.set('Content-Range', 'bytes 0-999/5000')
.send(partialData);
});
Redirection (3xx):
javascript
// 301 Moved Permanently
app.get('/old-users', (req, res) => {
res.status(301).redirect('/api/v1/users');
});
// 302 Found (Temporary redirect)
app.get('/temp-redirect', (req, res) => {
res.status(302).redirect('/temporary-page');
});
// 303 See Other - For POST results
app.post('/form-submit', (req, res) => {
// Process form
res.status(303).redirect('/success-page');
});
// 304 Not Modified - Cache validation
app.get('/cache-me', (req, res) => {
const etag = generateETag(data);
if (req.headers['if-none-match'] === etag) {
res.status(304).end();
} else {
res.set('ETag', etag).json(data);
}
});
// 307 Temporary Redirect - Preserve method
app.post('/temporary', (req, res) => {
res.status(307).redirect('/new-location');
});
// 308 Permanent Redirect - Preserve method
app.post('/permanent', (req, res) => {
res.status(308).redirect('/new-permanent-location');
});
Client Errors (4xx):
javascript
// 400 Bad Request - General client error
app.post('/validate', (req, res) => {
if (!req.body.email) {
return res.status(400).json({ error: 'Email is required' });
}
});
// 401 Unauthorized - Authentication needed
app.get('/secure', authenticate, (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
res.json(secureData);
});
// 403 Forbidden - Authenticated but not authorized
app.get('/admin', (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Insufficient permissions' });
}
res.json(adminData);
});
// 404 Not Found - Resource doesn't exist
app.get('/users/:id', (req, res) => {
const user = findUser(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// 405 Method Not Allowed
app.route('/users')
.get(/* handler */)
.post(/* handler */);
// PUT to /users returns 405
// 406 Not Acceptable - Can't provide requested content type
app.get('/data', (req, res) => {
const accept = req.headers.accept;
if (!accept.includes('application/json')) {
return res.status(406).json({
error: 'Only JSON responses available'
});
}
res.json(data);
});
// 409 Conflict - Resource conflict
app.post('/users', (req, res) => {
if (userExists(req.body.email)) {
return res.status(409).json({
error: 'User with this email already exists'
});
}
createUser(req.body);
});
// 410 Gone - Resource permanently removed
app.get('/old-api', (req, res) => {
res.status(410).json({
error: 'This API endpoint has been permanently removed',
message: 'Please use /api/v2 instead'
});
});
// 422 Unprocessable Entity - Semantic errors
app.post('/complex', validateComplex, (req, res) => {
const errors = validate(req.body);
if (errors.length > 0) {
return res.status(422).json({ errors });
}
// Process
});
// 429 Too Many Requests - Rate limiting
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
handler: (req, res) => {
res.status(429).json({
error: 'Too many requests',
retryAfter: 15 * 60 // seconds
});
}
}));
Server Errors (5xx):
javascript
// 500 Internal Server Error - Generic server error
app.get('/error', (req, res) => {
try {
throw new Error('Something went wrong');
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
}
});
// 501 Not Implemented
app.put('/future-feature', (req, res) => {
res.status(501).json({ error: 'Not implemented yet' });
});
// 502 Bad Gateway - Proxy/upstream error
app.get('/proxy-data', async (req, res) => {
try {
const data = await fetchFromUpstream();
res.json(data);
} catch (error) {
res.status(502).json({ error: 'Bad gateway' });
}
});
// 503 Service Unavailable - Maintenance/overload
app.use((req, res, next) => {
if (process.env.MAINTENANCE_MODE === 'true') {
return res.status(503)
.set('Retry-After', 3600)
.json({ error: 'Service undergoing maintenance' });
}
next();
});
// 504 Gateway Timeout
app.get('/slow-resource', async (req, res) => {
const timeout = setTimeout(() => {
res.status(504).json({ error: 'Gateway timeout' });
}, 10000);
try {
const data = await slowOperation();
clearTimeout(timeout);
res.json(data);
} catch (error) {
clearTimeout(timeout);
res.status(500).json({ error: 'Internal error' });
}
});
API Versioning
1. URL Path Versioning (Most Common)
javascript
// routes/v1/userRoutes.js
const express = require('express');
const router = express.Router();
router.get('/users', (req, res) => {
// Version 1 logic
res.json({ version: 'v1', users: [] });
});
module.exports = router;
// routes/v2/userRoutes.js
const express = require('express');
const router = express.Router();
router.get('/users', (req, res) => {
// Version 2 logic with new features
res.json({
version: 'v2',
users: [],
metadata: { count: 0 }
});
});
module.exports = router;
// app.js
app.use('/api/v1/users', require('./routes/v1/userRoutes'));
app.use('/api/v2/users', require('./routes/v2/userRoutes'));
// Also support latest version alias
app.use('/api/users', require('./routes/v2/userRoutes'));
2. Header Versioning
javascript
// Versioning via Accept header
app.use('/api/users', (req, res, next) => {
const acceptHeader = req.headers.accept || '';
const versionMatch = acceptHeader.match(/version=(\d+)/);
const version = versionMatch ? versionMatch[1] : '1';
if (version === '1') {
require('./routes/v1/userRoutes')(req, res, next);
} else if (version === '2') {
require('./routes/v2/userRoutes')(req, res, next);
} else {
res.status(400).json({ error: 'Unsupported API version' });
}
});
// Custom header
app.use('/api/users', (req, res, next) => {
const apiVersion = req.headers['x-api-version'] || '1';
if (apiVersion === '1') {
// v1 logic
} else if (apiVersion === '2') {
// v2 logic
}
});
3. Query Parameter Versioning
javascript
app.use('/api/users', (req, res, next) => {
const version = req.query.api_version || '1';
if (version === '1') {
// v1 logic
} else if (version === '2') {
// v2 logic
} else {
res.status(400).json({ error: 'Invalid API version' });
}
});
// Usage: GET /api/users?api_version=2
4. Comprehensive Versioning Strategy:
javascript
// versioning.js - Version negotiation middleware
const versioning = (options = {}) => {
const {
defaultVersion = '1',
supportedVersions = ['1', '2'],
headerName = 'x-api-version',
paramName = 'api_version',
strict = false
} = options;
return (req, res, next) => {
let version = defaultVersion;
// 1. Check URL path first (highest priority)
const pathMatch = req.path.match(/\/v(\d+)\//);
if (pathMatch) {
version = pathMatch[1];
}
// 2. Check custom header
else if (req.headers[headerName]) {
version = req.headers[headerName];
}
// 3. Check query parameter
else if (req.query[paramName]) {
version = req.query[paramName];
}
// 4. Check Accept header
else if (req.headers.accept) {
const acceptMatch = req.headers.accept.match(/version=(\d+)/);
if (acceptMatch) {
version = acceptMatch[1];
}
}
// Validate version
if (strict && !supportedVersions.includes(version)) {
return res.status(400).json({
error: 'Unsupported API version',
supportedVersions,
defaultVersion
});
}
// Normalize to supported version
if (!supportedVersions.includes(version)) {
version = defaultVersion;
}
// Attach version to request
req.apiVersion = version;
next();
};
};
// Usage
app.use('/api', versioning({
defaultVersion: '2',
supportedVersions: ['1', '2', '3']
}));
app.get('/api/users', (req, res) => {
if (req.apiVersion === '1') {
// v1 response
res.json({ version: 'v1', users: [] });
} else if (req.apiVersion === '2') {
// v2 response
res.json({ version: 'v2', users: [], metadata: {} });
} else {
// v3 response
res.json({ version: 'v3', data: { users: [] } });
}
});
5. Version Deprecation Strategy:
javascript
// Middleware to handle deprecated versions
const deprecationNotice = (version, sunsetDate) => {
return (req, res, next) => {
if (req.apiVersion === version) {
res.set({
'Deprecation': `version="${version}"`,
'Sunset': new Date(sunsetDate).toUTCString(),
'Link': '</api/v2/users>; rel="successor-version"',
'Warning': `299 - "Deprecated API version ${version}. Migrate to v2 by ${sunsetDate}"`
});
}
next();
};
};
// Usage
app.use('/api', versioning());
app.use('/api', deprecationNotice('1', '2024-12-31'));
// Version migration guide endpoint
app.get('/api/migration-guide', (req, res) => {
res.json({
title: 'API Migration Guide',
versions: [
{
version: 'v1',
status: 'deprecated',
sunsetDate: '2024-12-31',
changes: [
'Removed field: user.age',
'Added field: user.dateOfBirth',
'Changed endpoint: POST /login to POST /auth/login'
]
},
{
version: 'v2',
status: 'current',
changes: [
'Added pagination metadata',
'Added HATEOAS links',
'Improved error responses'
]
}
]
});
});
Complete REST API with Versioning:
javascript
// project structure
project/
├── src/
│ ├── api/
│ │ ├── v1/
│ │ │ ├── controllers/
│ │ │ ├── models/
│ │ │ ├── routes/
│ │ │ └── index.js
│ │ ├── v2/
│ │ │ ├── controllers/
│ │ │ ├── models/
│ │ │ ├── routes/
│ │ │ └── index.js
│ │ └── middleware/
│ ├── config/
│ └── app.js
└── package.json
// src/api/v1/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./routes/userRoutes');
const productRoutes = require('./routes/productRoutes');
router.use('/users', userRoutes);
router.use('/products', productRoutes);
module.exports = router;
// src/api/v2/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./routes/userRoutes');
const productRoutes = require('./routes/productRoutes');
router.use('/users', userRoutes);
router.use('/products', productRoutes);
// New v2-only routes
router.use('/analytics', require('./routes/analyticsRoutes'));
module.exports = router;
// src/app.js
const express = require('express');
const app = express();
// Import versions
const v1Routes = require('./api/v1');
const v2Routes = require('./api/v2');
// Mount versioned routes
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// Default to latest version
app.use('/api', v2Routes);
// API info endpoint
app.get('/api', (req, res) => {
res.json({
api: {
name: 'My REST API',
versions: {
v1: {
status: 'deprecated',
endpoint: '/api/v1',
sunset: '2024-12-31'
},
v2: {
status: 'current',
endpoint: '/api/v2'
}
},
documentation: 'https://api.example.com/docs'
}
});
});
// Test the API
// GET /api/v1/users → v1 users
// GET /api/v2/users → v2 users
// GET /api/users → v2 users (default)
// GET /api → API information
This comprehensive guide covers RESTful API design principles, implementation patterns, status code usage, and versioning strategies for building professional-grade APIs with Node.js and Express.
Comments (0)
No comments yet. Be the first to comment!