Master professional RESTful API design and implementation. Learn HTTP methods, status codes, resource naming, API versioning, and HATEOAS.
Master professional RESTful API design and implementation. Learn HTTP methods, status codes, resource naming, API versioning, and HATEOAS.
Understand REST architectural principles and constraints
Master HTTP methods, status codes, and headers
Design intuitive resource naming conventions
Implement effective API versioning strategies
Apply HATEOAS for self-describing APIs
Follow Richardson Maturity Model for API evolution
REST (Representational State Transfer) is an architectural style that defines constraints for creating scalable web services.
Learn about Six REST Constraints.
1/*
21. CLIENT-SERVER ARCHITECTURE
3 - Separation of concerns between client and server
4 - Server manages data, client manages user interface
5 - Enables independent evolution of both sides
6
72. STATELESSNESS
8 - Each request contains all information needed to process it
9 - Server doesn't store client state between requests
10 - Improves scalability and reliability
11
123. CACHEABILITY
13 - Responses must explicitly indicate if they're cacheable
14 - Improves performance and reduces server load
15 - Clients can reuse cached responses
16
174. UNIFORM INTERFACE
18 - Consistent way to interact with resources
19 - Standard HTTP methods and status codes
20 - Self-descriptive messages
21
225. LAYERED SYSTEM
23 - Client doesn't need to know if connected directly to server
24 - Supports load balancers, proxies, gateways
25 - Improves scalability and security
26
276. CODE ON DEMAND (Optional)
28 - Server can send executable code to client
29 - Examples: JavaScript, applets
30 - Extends client functionality
31*/
32
33// Example of RESTful resource design
34const apiDesign = {
35 // Resource identification through URIs
36 resources: {
37 users: '/api/v1/users',
38 userById: '/api/v1/users/{id}',
39 userOrders: '/api/v1/users/{id}/orders',
40 orders: '/api/v1/orders',
41 orderById: '/api/v1/orders/{id}',
42 products: '/api/v1/products',
43 productById: '/api/v1/products/{id}'
44 },
45
46 // Standard HTTP methods for different operations
47 methods: {
48 GET: 'Retrieve resource(s)',
49 POST: 'Create new resource',
50 PUT: 'Update/replace entire resource',
51 PATCH: 'Partially update resource',
52 DELETE: 'Remove resource',
53 HEAD: 'Get resource metadata only',
54 OPTIONS: 'Get allowed methods for resource'
55 },
56
57 // Standard HTTP status codes
58 statusCodes: {
59 // Success
60 200: 'OK - Request successful',
61 201: 'Created - Resource created',
62 202: 'Accepted - Request accepted for processing',
63 204: 'No Content - Successful but no response body',
64
65 // Client Errors
66 400: 'Bad Request - Invalid request syntax',
67 401: 'Unauthorized - Authentication required',
68 403: 'Forbidden - Access denied',
69 404: 'Not Found - Resource not found',
70 405: 'Method Not Allowed - HTTP method not supported',
71 409: 'Conflict - Resource conflict',
72 422: 'Unprocessable Entity - Validation error',
73 429: 'Too Many Requests - Rate limit exceeded',
74
75 // Server Errors
76 500: 'Internal Server Error - Generic server error',
77 502: 'Bad Gateway - Invalid response from upstream',
78 503: 'Service Unavailable - Server temporarily unavailable',
79 504: 'Gateway Timeout - Upstream timeout'
80 }
81};Design a comprehensive RESTful API following industry best practices and conventions.
1const express = require('express');
2const cors = require('cors');
3const helmet = require('helmet');
4const rateLimit = require('express-rate-limit');
5
6// Import route modules
7const userRoutes = require('./users');
8const productRoutes = require('./products');
9const orderRoutes = require('./orders');
10const categoryRoutes = require('./categories');
11
12// Import middleware
13const auth = require('../../../middleware/auth');
14const validation = require('../../../middleware/validation');
15const errorHandler = require('../../../middleware/errorHandler');
16const requestLogger = require('../../../middleware/requestLogger');
17
18class APIRouter {
19 constructor() {
20 this.router = express.Router();
21 this.setupMiddleware();
22 this.setupRoutes();
23 this.setupErrorHandling();
24 }
25
26 setupMiddleware() {
27 // Security middleware
28 this.router.use(helmet({
29 contentSecurityPolicy: false, // Allow for API usage
30 crossOriginResourcePolicy: { policy: "cross-origin" }
31 }));
32
33 // CORS configuration
34 this.router.use(cors({
35 origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
36 methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
37 allowedHeaders: [
38 'Content-Type',
39 'Authorization',
40 'X-Requested-With',
41 'X-API-Version',
42 'X-Client-Version'
43 ],
44 exposedHeaders: [
45 'X-Total-Count',
46 'X-Page-Count',
47 'X-Rate-Limit-Remaining',
48 'X-Rate-Limit-Reset'
49 ],
50 credentials: true,
51 maxAge: 86400 // 24 hours
52 }));
53
54 // Rate limiting
55 const limiter = rateLimit({
56 windowMs: 15 * 60 * 1000, // 15 minutes
57 max: 1000, // Limit each IP to 1000 requests per windowMs
58 message: {
59 error: 'Too many requests',
60 message: 'Rate limit exceeded. Please try again later.',
61 retryAfter: 900 // 15 minutes in seconds
62 },
63 standardHeaders: true,
64 legacyHeaders: false,
65 handler: (req, res) => {
66 res.status(429).json({
67 error: 'Too Many Requests',
68 message: 'Rate limit exceeded',
69 retryAfter: Math.round(req.rateLimit.resetTime / 1000)
70 });
71 }
72 });
73
74 this.router.use(limiter);
75
76 // Request parsing
77 this.router.use(express.json({ limit: '10mb' }));
78 this.router.use(express.urlencoded({ extended: true, limit: '10mb' }));
79
80 // Request logging
81 this.router.use(requestLogger);
82
83 // API versioning header validation
84 this.router.use((req, res, next) => {
85 const acceptedVersions = ['1.0', '1.1'];
86 const requestedVersion = req.headers['x-api-version'] || '1.0';
87
88 if (!acceptedVersions.includes(requestedVersion)) {
89 return res.status(400).json({
90 error: 'Unsupported API Version',
91 message: \`API version \${requestedVersion} is not supported\`,
92 supportedVersions: acceptedVersions
93 });
94 }
95
96 req.apiVersion = requestedVersion;
97 res.set('X-API-Version', requestedVersion);
98 next();
99 });
100 }
101
102 setupRoutes() {
103 // Health check endpoint
104 this.router.get('/health', (req, res) => {
105 res.json({
106 status: 'healthy',
107 timestamp: new Date().toISOString(),
108 version: process.env.API_VERSION || '1.0.0',
109 environment: process.env.NODE_ENV || 'development',
110 uptime: process.uptime()
111 });
112 });
113
114 // API documentation endpoint
115 this.router.get('/docs', (req, res) => {
116 res.json({
117 name: 'E-commerce API',
118 version: '1.0.0',
119 description: 'RESTful API for e-commerce platform',
120 documentation: '/api/v1/swagger',
121 endpoints: {
122 users: '/api/v1/users',
123 products: '/api/v1/products',
124 orders: '/api/v1/orders',
125 categories: '/api/v1/categories'
126 },
127 authentication: {
128 type: 'Bearer Token (JWT)',
129 header: 'Authorization: Bearer <token>'
130 },
131 rateLimit: {
132 requests: 1000,
133 window: '15 minutes'
134 }
135 });
136 });
137
138 // Resource routes
139 this.router.use('/users', userRoutes);
140 this.router.use('/products', productRoutes);
141 this.router.use('/orders', orderRoutes);
142 this.router.use('/categories', categoryRoutes);
143
144 // Catch unmatched routes
145 this.router.use('*', (req, res) => {
146 res.status(404).json({
147 error: 'Not Found',
148 message: \`The endpoint \${req.method} \${req.originalUrl} was not found\`,
149 suggestion: 'Check the API documentation at /api/v1/docs'
150 });
151 });
152 }
153
154 setupErrorHandling() {
155 this.router.use(errorHandler);
156 }
157
158 getRouter() {
159 return this.router;
160 }
161}
162
163module.exports = new APIRouter().getRouter();Follow consistent and intuitive naming patterns for resources and endpoints.
1const express = require('express');
2const router = express.Router();
3const auth = require('../../../middleware/auth');
4const validation = require('../../../middleware/validation');
5const productController = require('../../../controllers/productController');
6const reviewController = require('../../../controllers/reviewController');
7
8/*
9RESOURCE NAMING CONVENTIONS:
10
111. Use nouns, not verbs
12 ✅ /products (good)
13 ❌ /getProducts (bad)
14
152. Use plural nouns for collections
16 ✅ /products (good)
17 ❌ /product (bad)
18
193. Use hierarchical structure for relationships
20 ✅ /products/123/reviews (good)
21 ❌ /productReviews?productId=123 (acceptable but not ideal)
22
234. Use lowercase and hyphens for multi-word resources
24 ✅ /product-categories (good)
25 ❌ /productCategories or /product_categories (bad)
26
275. Keep URLs short and intuitive
28 ✅ /products/123/variants (good)
29 ❌ /products/123/productVariants (bad)
30*/
31
32// Products Collection Endpoints
33// GET /api/v1/products - Get all products with filtering, sorting, pagination
34router.get('/',
35 validation.validateQueryParams('productList'),
36 productController.getProducts
37);
38
39// POST /api/v1/products - Create a new product
40router.post('/',
41 auth.requireAuth,
42 auth.requireRole(['admin', 'manager']),
43 validation.validateBody('createProduct'),
44 productController.createProduct
45);
46
47// Product Resource Endpoints
48// GET /api/v1/products/:id - Get specific product
49router.get('/:id',
50 validation.validateParams('productId'),
51 productController.getProductById
52);
53
54// PUT /api/v1/products/:id - Update entire product (replace)
55router.put('/:id',
56 auth.requireAuth,
57 auth.requireRole(['admin', 'manager']),
58 validation.validateParams('productId'),
59 validation.validateBody('updateProduct'),
60 productController.updateProduct
61);
62
63// PATCH /api/v1/products/:id - Partially update product
64router.patch('/:id',
65 auth.requireAuth,
66 auth.requireRole(['admin', 'manager']),
67 validation.validateParams('productId'),
68 validation.validateBody('patchProduct'),
69 productController.patchProduct
70);
71
72// DELETE /api/v1/products/:id - Delete product (soft delete)
73router.delete('/:id',
74 auth.requireAuth,
75 auth.requireRole(['admin']),
76 validation.validateParams('productId'),
77 productController.deleteProduct
78);
79
80// Product Variants (Nested Resource)
81// GET /api/v1/products/:id/variants - Get product variants
82router.get('/:id/variants',
83 validation.validateParams('productId'),
84 productController.getProductVariants
85);
86
87// POST /api/v1/products/:id/variants - Create product variant
88router.post('/:id/variants',
89 auth.requireAuth,
90 auth.requireRole(['admin', 'manager']),
91 validation.validateParams('productId'),
92 validation.validateBody('createVariant'),
93 productController.createProductVariant
94);
95
96// PUT /api/v1/products/:id/variants/:variantId - Update variant
97router.put('/:id/variants/:variantId',
98 auth.requireAuth,
99 auth.requireRole(['admin', 'manager']),
100 validation.validateParams('productVariantId'),
101 validation.validateBody('updateVariant'),
102 productController.updateProductVariant
103);
104
105// DELETE /api/v1/products/:id/variants/:variantId - Delete variant
106router.delete('/:id/variants/:variantId',
107 auth.requireAuth,
108 auth.requireRole(['admin', 'manager']),
109 validation.validateParams('productVariantId'),
110 productController.deleteProductVariant
111);
112
113// Product Reviews (Nested Resource)
114// GET /api/v1/products/:id/reviews - Get product reviews
115router.get('/:id/reviews',
116 validation.validateParams('productId'),
117 validation.validateQueryParams('reviewList'),
118 reviewController.getProductReviews
119);
120
121// POST /api/v1/products/:id/reviews - Create review for product
122router.post('/:id/reviews',
123 auth.requireAuth,
124 validation.validateParams('productId'),
125 validation.validateBody('createReview'),
126 reviewController.createProductReview
127);
128
129// Product Images (Nested Resource)
130// GET /api/v1/products/:id/images - Get product images
131router.get('/:id/images',
132 validation.validateParams('productId'),
133 productController.getProductImages
134);
135
136// POST /api/v1/products/:id/images - Upload product images
137router.post('/:id/images',
138 auth.requireAuth,
139 auth.requireRole(['admin', 'manager']),
140 validation.validateParams('productId'),
141 productController.uploadProductImages
142);
143
144// DELETE /api/v1/products/:id/images/:imageId - Delete product image
145router.delete('/:id/images/:imageId',
146 auth.requireAuth,
147 auth.requireRole(['admin', 'manager']),
148 validation.validateParams('productImageId'),
149 productController.deleteProductImage
150);
151
152// Bulk Operations
153// POST /api/v1/products/bulk - Bulk create products
154router.post('/bulk',
155 auth.requireAuth,
156 auth.requireRole(['admin']),
157 validation.validateBody('bulkCreateProducts'),
158 productController.bulkCreateProducts
159);
160
161// PATCH /api/v1/products/bulk - Bulk update products
162router.patch('/bulk',
163 auth.requireAuth,
164 auth.requireRole(['admin']),
165 validation.validateBody('bulkUpdateProducts'),
166 productController.bulkUpdateProducts
167);
168
169// Special Actions (Non-CRUD operations)
170// POST /api/v1/products/:id/duplicate - Duplicate a product
171router.post('/:id/duplicate',
172 auth.requireAuth,
173 auth.requireRole(['admin', 'manager']),
174 validation.validateParams('productId'),
175 productController.duplicateProduct
176);
177
178// POST /api/v1/products/:id/archive - Archive a product
179router.post('/:id/archive',
180 auth.requireAuth,
181 auth.requireRole(['admin', 'manager']),
182 validation.validateParams('productId'),
183 productController.archiveProduct
184);
185
186// POST /api/v1/products/:id/restore - Restore archived product
187router.post('/:id/restore',
188 auth.requireAuth,
189 auth.requireRole(['admin']),
190 validation.validateParams('productId'),
191 productController.restoreProduct
192);
193
194// Search endpoint (collection-level operation)
195// GET /api/v1/products/search?q=query - Search products
196router.get('/search',
197 validation.validateQueryParams('productSearch'),
198 productController.searchProducts
199);
200
201module.exports = router;Implement proper HTTP methods and return appropriate status codes for different operations.
1const productService = require('../services/productService');
2const httpStatus = require('http-status');
3
4class ProductController {
5 // GET /api/v1/products - Retrieve products collection
6 async getProducts(req, res, next) {
7 try {
8 const {
9 page = 1,
10 limit = 20,
11 sortBy = 'createdAt',
12 sortOrder = 'desc',
13 category,
14 priceMin,
15 priceMax,
16 inStock,
17 featured,
18 search
19 } = req.query;
20
21 const filters = {
22 category,
23 priceRange: (priceMin || priceMax) ? { min: priceMin, max: priceMax } : null,
24 inStock: inStock !== undefined ? inStock === 'true' : null,
25 featured: featured !== undefined ? featured === 'true' : null,
26 search
27 };
28
29 const pagination = {
30 page: parseInt(page),
31 limit: parseInt(limit),
32 sortBy,
33 sortOrder
34 };
35
36 const result = await productService.getProducts(filters, pagination);
37
38 // Set pagination headers
39 res.set({
40 'X-Total-Count': result.pagination.totalCount.toString(),
41 'X-Page-Count': result.pagination.totalPages.toString(),
42 'X-Current-Page': result.pagination.currentPage.toString(),
43 'X-Per-Page': result.pagination.limit.toString()
44 });
45
46 // Add pagination links
47 const baseUrl = \`\${req.protocol}://\${req.get('Host')}\${req.baseUrl}\${req.path}\`;
48 const links = [];
49
50 if (result.pagination.hasPrevPage) {
51 links.push(\`<\${baseUrl}?page=\${result.pagination.currentPage - 1}&limit=\${limit}>; rel="prev"\`);
52 links.push(\`<\${baseUrl}?page=1&limit=\${limit}>; rel="first"\`);
53 }
54
55 if (result.pagination.hasNextPage) {
56 links.push(\`<\${baseUrl}?page=\${result.pagination.currentPage + 1}&limit=\${limit}>; rel="next"\`);
57 links.push(\`<\${baseUrl}?page=\${result.pagination.totalPages}&limit=\${limit}>; rel="last"\`);
58 }
59
60 if (links.length > 0) {
61 res.set('Link', links.join(', '));
62 }
63
64 // 200 OK - Successful retrieval
65 res.status(httpStatus.OK).json({
66 success: true,
67 data: result.products,
68 pagination: result.pagination,
69 filters: filters,
70 meta: {
71 timestamp: new Date().toISOString(),
72 version: req.apiVersion
73 }
74 });
75
76 } catch (error) {
77 next(error);
78 }
79 }
80
81 // POST /api/v1/products - Create new product
82 async createProduct(req, res, next) {
83 try {
84 const productData = {
85 ...req.body,
86 createdBy: req.user.id
87 };
88
89 const result = await productService.createProduct(productData);
90
91 // Set location header for newly created resource
92 res.set('Location', \`/api/v1/products/\${result.product.id}\`);
93
94 // 201 Created - Resource successfully created
95 res.status(httpStatus.CREATED).json({
96 success: true,
97 data: result.product,
98 message: 'Product created successfully',
99 meta: {
100 timestamp: new Date().toISOString(),
101 createdBy: req.user.email
102 }
103 });
104
105 } catch (error) {
106 if (error.name === 'ValidationError') {
107 // 422 Unprocessable Entity - Validation failed
108 return res.status(httpStatus.UNPROCESSABLE_ENTITY).json({
109 success: false,
110 error: 'Validation Error',
111 message: error.message,
112 details: error.details || {}
113 });
114 }
115
116 if (error.code === 'DUPLICATE_SKU') {
117 // 409 Conflict - Resource already exists
118 return res.status(httpStatus.CONFLICT).json({
119 success: false,
120 error: 'Conflict',
121 message: 'Product with this SKU already exists',
122 conflictField: 'sku'
123 });
124 }
125
126 next(error);
127 }
128 }
129
130 // GET /api/v1/products/:id - Retrieve specific product
131 async getProductById(req, res, next) {
132 try {
133 const { id } = req.params;
134 const { include } = req.query;
135
136 const includeOptions = {
137 variants: include?.includes('variants'),
138 reviews: include?.includes('reviews'),
139 category: include?.includes('category'),
140 images: include?.includes('images')
141 };
142
143 const result = await productService.getProductById(id, includeOptions);
144
145 if (!result.product) {
146 // 404 Not Found - Resource doesn't exist
147 return res.status(httpStatus.NOT_FOUND).json({
148 success: false,
149 error: 'Not Found',
150 message: \`Product with ID \${id} was not found\`
151 });
152 }
153
154 // Add caching headers for GET requests
155 res.set({
156 'Cache-Control': 'public, max-age=300', // 5 minutes
157 'ETag': \`"\${result.product.updatedAt}"\`,
158 'Last-Modified': new Date(result.product.updatedAt).toUTCString()
159 });
160
161 // Check if client has cached version
162 if (req.headers['if-none-match'] === \`"\${result.product.updatedAt}"\`) {
163 // 304 Not Modified - Client has current version
164 return res.status(httpStatus.NOT_MODIFIED).end();
165 }
166
167 // 200 OK - Successful retrieval
168 res.status(httpStatus.OK).json({
169 success: true,
170 data: result.product,
171 meta: {
172 timestamp: new Date().toISOString(),
173 included: includeOptions
174 }
175 });
176
177 } catch (error) {
178 next(error);
179 }
180 }
181
182 // PUT /api/v1/products/:id - Update entire product (replace)
183 async updateProduct(req, res, next) {
184 try {
185 const { id } = req.params;
186 const updateData = {
187 ...req.body,
188 updatedBy: req.user.id
189 };
190
191 const result = await productService.updateProduct(id, updateData, { replace: true });
192
193 if (!result.product) {
194 // 404 Not Found - Resource doesn't exist
195 return res.status(httpStatus.NOT_FOUND).json({
196 success: false,
197 error: 'Not Found',
198 message: \`Product with ID \${id} was not found\`
199 });
200 }
201
202 // 200 OK - Successful update
203 res.status(httpStatus.OK).json({
204 success: true,
205 data: result.product,
206 message: 'Product updated successfully',
207 meta: {
208 timestamp: new Date().toISOString(),
209 updatedBy: req.user.email,
210 operation: 'replace'
211 }
212 });
213
214 } catch (error) {
215 if (error.name === 'ValidationError') {
216 return res.status(httpStatus.UNPROCESSABLE_ENTITY).json({
217 success: false,
218 error: 'Validation Error',
219 message: error.message,
220 details: error.details || {}
221 });
222 }
223
224 next(error);
225 }
226 }
227
228 // PATCH /api/v1/products/:id - Partially update product
229 async patchProduct(req, res, next) {
230 try {
231 const { id } = req.params;
232 const updateData = {
233 ...req.body,
234 updatedBy: req.user.id
235 };
236
237 const result = await productService.updateProduct(id, updateData, { replace: false });
238
239 if (!result.product) {
240 return res.status(httpStatus.NOT_FOUND).json({
241 success: false,
242 error: 'Not Found',
243 message: \`Product with ID \${id} was not found\`
244 });
245 }
246
247 // 200 OK - Successful partial update
248 res.status(httpStatus.OK).json({
249 success: true,
250 data: result.product,
251 message: 'Product updated successfully',
252 meta: {
253 timestamp: new Date().toISOString(),
254 updatedBy: req.user.email,
255 operation: 'patch',
256 fieldsUpdated: Object.keys(req.body)
257 }
258 });
259
260 } catch (error) {
261 if (error.name === 'ValidationError') {
262 return res.status(httpStatus.UNPROCESSABLE_ENTITY).json({
263 success: false,
264 error: 'Validation Error',
265 message: error.message,
266 details: error.details || {}
267 });
268 }
269
270 next(error);
271 }
272 }
273
274 // DELETE /api/v1/products/:id - Delete product
275 async deleteProduct(req, res, next) {
276 try {
277 const { id } = req.params;
278 const { permanent = false } = req.query;
279
280 const result = await productService.deleteProduct(id, {
281 permanent: permanent === 'true',
282 deletedBy: req.user.id
283 });
284
285 if (!result.success) {
286 return res.status(httpStatus.NOT_FOUND).json({
287 success: false,
288 error: 'Not Found',
289 message: \`Product with ID \${id} was not found\`
290 });
291 }
292
293 if (permanent === 'true') {
294 // 204 No Content - Resource permanently deleted
295 res.status(httpStatus.NO_CONTENT).end();
296 } else {
297 // 200 OK - Resource soft deleted
298 res.status(httpStatus.OK).json({
299 success: true,
300 message: 'Product archived successfully',
301 meta: {
302 timestamp: new Date().toISOString(),
303 deletedBy: req.user.email,
304 permanent: false,
305 restoreEndpoint: \`/api/v1/products/\${id}/restore\`
306 }
307 });
308 }
309
310 } catch (error) {
311 if (error.code === 'CONSTRAINT_VIOLATION') {
312 // 409 Conflict - Cannot delete due to dependencies
313 return res.status(httpStatus.CONFLICT).json({
314 success: false,
315 error: 'Conflict',
316 message: 'Cannot delete product with existing orders',
317 suggestion: 'Archive the product instead or handle dependent records first'
318 });
319 }
320
321 next(error);
322 }
323 }
324
325 // HEAD /api/v1/products/:id - Get product metadata
326 async getProductMetadata(req, res, next) {
327 try {
328 const { id } = req.params;
329 const result = await productService.getProductMetadata(id);
330
331 if (!result.exists) {
332 return res.status(httpStatus.NOT_FOUND).end();
333 }
334
335 // Set headers without response body
336 res.set({
337 'Content-Type': 'application/json',
338 'Content-Length': JSON.stringify(result.metadata).length.toString(),
339 'Last-Modified': new Date(result.metadata.updatedAt).toUTCString(),
340 'ETag': \`"\${result.metadata.updatedAt}"\`
341 });
342
343 // 200 OK - Headers only, no body
344 res.status(httpStatus.OK).end();
345
346 } catch (error) {
347 next(error);
348 }
349 }
350
351 // OPTIONS /api/v1/products/:id - Get allowed methods
352 async getProductOptions(req, res, next) {
353 try {
354 const { id } = req.params;
355
356 // Check if resource exists
357 const exists = await productService.productExists(id);
358
359 if (!exists) {
360 return res.status(httpStatus.NOT_FOUND).end();
361 }
362
363 // Set allowed methods based on user permissions
364 let allowedMethods = ['GET', 'HEAD', 'OPTIONS'];
365
366 if (req.user) {
367 if (req.user.role === 'admin' || req.user.role === 'manager') {
368 allowedMethods.push('PUT', 'PATCH', 'DELETE');
369 }
370 }
371
372 res.set({
373 'Allow': allowedMethods.join(', '),
374 'Access-Control-Allow-Methods': allowedMethods.join(', ')
375 });
376
377 // 200 OK - Options response
378 res.status(httpStatus.OK).json({
379 allowedMethods,
380 endpoints: {
381 self: \`/api/v1/products/\${id}\`,
382 variants: \`/api/v1/products/\${id}/variants\`,
383 reviews: \`/api/v1/products/\${id}/reviews\`,
384 images: \`/api/v1/products/\${id}/images\`
385 }
386 });
387
388 } catch (error) {
389 next(error);
390 }
391 }
392}
393
394module.exports = new ProductController();Implement proper API versioning to maintain backward compatibility while allowing for evolution.
1class APIVersioningMiddleware {
2 constructor() {
3 this.supportedVersions = new Map([
4 ['1.0', {
5 released: '2024-01-01',
6 deprecated: null,
7 sunset: null,
8 features: ['basic-crud', 'authentication', 'pagination']
9 }],
10 ['1.1', {
11 released: '2024-06-01',
12 deprecated: null,
13 sunset: null,
14 features: ['basic-crud', 'authentication', 'pagination', 'search', 'bulk-operations']
15 }],
16 ['2.0', {
17 released: '2024-12-01',
18 deprecated: null,
19 sunset: null,
20 features: ['graphql', 'real-time', 'advanced-search', 'analytics']
21 }]
22 ]);
23
24 this.defaultVersion = '1.1';
25 this.deprecationWarnings = new Map([
26 ['1.0', 'API version 1.0 will be deprecated on 2025-06-01. Please upgrade to version 2.0.']
27 ]);
28 }
29
30 // Version detection middleware
31 detectVersion() {
32 return (req, res, next) => {
33 let version = this.defaultVersion;
34
35 // 1. Check Accept header version (preferred)
36 const acceptHeader = req.headers.accept;
37 if (acceptHeader && acceptHeader.includes('application/vnd.api+json')) {
38 const versionMatch = acceptHeader.match(/version=([0-9.]+)/);
39 if (versionMatch) {
40 version = versionMatch[1];
41 }
42 }
43
44 // 2. Check custom header
45 if (req.headers['x-api-version']) {
46 version = req.headers['x-api-version'];
47 }
48
49 // 3. Check URL path (last resort)
50 const pathVersionMatch = req.path.match(/\\/v([0-9.]+)\\//);
51 if (pathVersionMatch) {
52 version = pathVersionMatch[1];
53 }
54
55 // 4. Check query parameter (not recommended but supported)
56 if (req.query.version) {
57 version = req.query.version;
58 }
59
60 // Validate version
61 if (!this.supportedVersions.has(version)) {
62 return res.status(400).json({
63 error: 'Unsupported API Version',
64 message: \`API version \${version} is not supported\`,
65 supportedVersions: Array.from(this.supportedVersions.keys()),
66 currentVersion: this.defaultVersion
67 });
68 }
69
70 // Set version info
71 req.apiVersion = version;
72 req.versionInfo = this.supportedVersions.get(version);
73
74 // Set response headers
75 res.set({
76 'X-API-Version': version,
77 'X-API-Version-Released': req.versionInfo.released
78 });
79
80 // Add deprecation warning if applicable
81 if (this.deprecationWarnings.has(version)) {
82 res.set('X-API-Deprecation-Warning', this.deprecationWarnings.get(version));
83 res.set('X-API-Sunset-Date', req.versionInfo.sunset || 'TBD');
84 }
85
86 next();
87 };
88 }
89
90 // Version-specific feature checking
91 checkFeature(feature) {
92 return (req, res, next) => {
93 if (!req.versionInfo.features.includes(feature)) {
94 return res.status(404).json({
95 error: 'Feature Not Available',
96 message: \`Feature '\${feature}' is not available in API version \${req.apiVersion}\`,
97 availableIn: this.getVersionsWithFeature(feature),
98 upgradeRecommendation: this.getLatestVersionWithFeature(feature)
99 });
100 }
101 next();
102 };
103 }
104
105 // Version-specific response transformation
106 transformResponse(version, data) {
107 switch (version) {
108 case '1.0':
109 return this.transformToV1_0(data);
110 case '1.1':
111 return this.transformToV1_1(data);
112 case '2.0':
113 return this.transformToV2_0(data);
114 default:
115 return data;
116 }
117 }
118
119 transformToV1_0(data) {
120 // Legacy format for v1.0
121 if (Array.isArray(data)) {
122 return data.map(item => ({
123 id: item.id,
124 name: item.name,
125 price: item.price,
126 description: item.description,
127 // Remove newer fields not available in v1.0
128 created_at: item.createdAt,
129 updated_at: item.updatedAt
130 }));
131 }
132
133 return {
134 id: data.id,
135 name: data.name,
136 price: data.price,
137 description: data.description,
138 created_at: data.createdAt,
139 updated_at: data.updatedAt
140 };
141 }
142
143 transformToV1_1(data) {
144 // Enhanced format for v1.1
145 if (Array.isArray(data)) {
146 return data.map(item => ({
147 id: item.id,
148 name: item.name,
149 slug: item.slug,
150 price: item.price,
151 compareAtPrice: item.compareAtPrice,
152 description: item.description,
153 shortDescription: item.shortDescription,
154 category: item.category,
155 tags: item.tags,
156 images: item.images,
157 inStock: item.stockStatus === 'in_stock',
158 createdAt: item.createdAt,
159 updatedAt: item.updatedAt
160 }));
161 }
162
163 return {
164 id: data.id,
165 name: data.name,
166 slug: data.slug,
167 price: data.price,
168 compareAtPrice: data.compareAtPrice,
169 description: data.description,
170 shortDescription: data.shortDescription,
171 category: data.category,
172 tags: data.tags,
173 images: data.images,
174 inStock: data.stockStatus === 'in_stock',
175 createdAt: data.createdAt,
176 updatedAt: data.updatedAt
177 };
178 }
179
180 transformToV2_0(data) {
181 // Latest format with all features
182 return data; // No transformation needed for latest version
183 }
184
185 getVersionsWithFeature(feature) {
186 const versions = [];
187 for (const [version, info] of this.supportedVersions) {
188 if (info.features.includes(feature)) {
189 versions.push(version);
190 }
191 }
192 return versions;
193 }
194
195 getLatestVersionWithFeature(feature) {
196 const versions = this.getVersionsWithFeature(feature);
197 return versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))[0];
198 }
199
200 // Middleware to transform responses based on version
201 responseTransformer() {
202 return (req, res, next) => {
203 const originalJson = res.json;
204
205 res.json = (data) => {
206 // Transform data based on API version
207 const transformedData = this.transformResponse(req.apiVersion, data);
208
209 // Add version-specific metadata
210 const responseData = {
211 ...transformedData,
212 _meta: {
213 version: req.apiVersion,
214 timestamp: new Date().toISOString(),
215 requestId: req.id || 'unknown'
216 }
217 };
218
219 // Call original json method with transformed data
220 return originalJson.call(res, responseData);
221 };
222
223 next();
224 };
225 }
226
227 // Content negotiation based on version
228 contentNegotiation() {
229 return (req, res, next) => {
230 const accept = req.headers.accept || 'application/json';
231
232 // Set content type based on version and accept header
233 if (accept.includes('application/vnd.api+json')) {
234 res.set('Content-Type', \`application/vnd.api+json; version=\${req.apiVersion}\`);
235 } else if (accept.includes('application/hal+json') && req.apiVersion >= '2.0') {
236 res.set('Content-Type', 'application/hal+json');
237 } else {
238 res.set('Content-Type', 'application/json');
239 }
240
241 next();
242 };
243 }
244}
245
246module.exports = new APIVersioningMiddleware();Implement Hypermedia as the Engine of Application State for self-describing APIs.
1class HATEOASGenerator {
2 constructor(baseUrl = '') {
3 this.baseUrl = baseUrl;
4 }
5
6 // Generate links for a resource
7 generateLinks(resource, resourceType, userPermissions = []) {
8 const links = {};
9 const resourceId = resource.id;
10
11 switch (resourceType) {
12 case 'product':
13 links.self = { href: \`\${this.baseUrl}/api/v1/products/\${resourceId}\` };
14 links.category = { href: \`\${this.baseUrl}/api/v1/categories/\${resource.categoryId}\` };
15 links.variants = { href: \`\${this.baseUrl}/api/v1/products/\${resourceId}/variants\` };
16 links.reviews = {
17 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}/reviews\`,
18 templated: true,
19 params: {
20 page: { type: 'number', default: 1 },
21 limit: { type: 'number', default: 10 },
22 sort: { type: 'string', enum: ['rating', 'date', 'helpful'] }
23 }
24 };
25 links.images = { href: \`\${this.baseUrl}/api/v1/products/\${resourceId}/images\` };
26
27 // Conditional links based on permissions and state
28 if (userPermissions.includes('product:edit')) {
29 links.edit = {
30 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}\`,
31 method: 'PUT',
32 schema: \`\${this.baseUrl}/api/v1/schemas/product\`
33 };
34 links.patch = {
35 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}\`,
36 method: 'PATCH'
37 };
38 }
39
40 if (userPermissions.includes('product:delete')) {
41 links.delete = {
42 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}\`,
43 method: 'DELETE',
44 confirmationRequired: true
45 };
46 links.archive = {
47 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}/archive\`,
48 method: 'POST',
49 title: 'Archive this product'
50 };
51 }
52
53 if (userPermissions.includes('product:duplicate')) {
54 links.duplicate = {
55 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}/duplicate\`,
56 method: 'POST',
57 title: 'Create a copy of this product'
58 };
59 }
60
61 // State-dependent actions
62 if (resource.status === 'draft') {
63 links.publish = {
64 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}/publish\`,
65 method: 'POST',
66 title: 'Publish this product'
67 };
68 }
69
70 if (resource.status === 'published') {
71 links.unpublish = {
72 href: \`\${this.baseUrl}/api/v1/products/\${resourceId}/unpublish\`,
73 method: 'POST',
74 title: 'Unpublish this product'
75 };
76 }
77
78 // Shopping cart actions
79 if (resource.stockStatus === 'in_stock') {
80 links.addToCart = {
81 href: \`\${this.baseUrl}/api/v1/cart/items\`,
82 method: 'POST',
83 schema: {
84 type: 'object',
85 properties: {
86 productId: { type: 'string', const: resourceId },
87 quantity: { type: 'number', minimum: 1, maximum: resource.maxQuantity || 10 },
88 variantId: { type: 'string', optional: true }
89 }
90 }
91 };
92 }
93
94 break;
95
96 case 'order':
97 links.self = { href: \`\${this.baseUrl}/api/v1/orders/\${resourceId}\` };
98 links.customer = { href: \`\${this.baseUrl}/api/v1/users/\${resource.userId}\` };
99 links.items = { href: \`\${this.baseUrl}/api/v1/orders/\${resourceId}/items\` };
100
101 // Order status transitions
102 const statusTransitions = this.getOrderStatusTransitions(resource.status, userPermissions);
103 if (statusTransitions.length > 0) {
104 links.actions = statusTransitions.map(transition => ({
105 rel: transition.action,
106 href: \`\${this.baseUrl}/api/v1/orders/\${resourceId}/\${transition.action}\`,
107 method: 'POST',
108 title: transition.title,
109 confirmationRequired: transition.requiresConfirmation || false
110 }));
111 }
112
113 // Payment links
114 if (resource.paymentStatus === 'pending') {
115 links.payment = {
116 href: \`\${this.baseUrl}/api/v1/orders/\${resourceId}/payment\`,
117 method: 'POST',
118 title: 'Process payment for this order'
119 };
120 }
121
122 if (resource.paymentStatus === 'completed' && userPermissions.includes('order:refund')) {
123 links.refund = {
124 href: \`\${this.baseUrl}/api/v1/orders/\${resourceId}/refund\`,
125 method: 'POST',
126 title: 'Issue refund for this order',
127 confirmationRequired: true
128 };
129 }
130
131 // Shipping links
132 if (resource.status === 'processing') {
133 links.ship = {
134 href: \`\${this.baseUrl}/api/v1/orders/\${resourceId}/ship\`,
135 method: 'POST',
136 schema: {
137 type: 'object',
138 properties: {
139 trackingNumber: { type: 'string' },
140 carrier: { type: 'string', enum: ['ups', 'fedex', 'usps', 'dhl'] },
141 estimatedDelivery: { type: 'string', format: 'date' }
142 }
143 }
144 };
145 }
146
147 if (resource.trackingNumber) {
148 links.tracking = {
149 href: \`\${this.baseUrl}/api/v1/orders/\${resourceId}/tracking\`,
150 title: 'Track this shipment'
151 };
152 }
153
154 break;
155
156 case 'user':
157 links.self = { href: \`\${this.baseUrl}/api/v1/users/\${resourceId}\` };
158 links.orders = {
159 href: \`\${this.baseUrl}/api/v1/users/\${resourceId}/orders\`,
160 templated: true
161 };
162 links.reviews = { href: \`\${this.baseUrl}/api/v1/users/\${resourceId}/reviews\` };
163
164 if (userPermissions.includes('user:edit') || resource.id === resourceId) {
165 links.edit = {
166 href: \`\${this.baseUrl}/api/v1/users/\${resourceId}\`,
167 method: 'PUT'
168 };
169 links.changePassword = {
170 href: \`\${this.baseUrl}/api/v1/users/\${resourceId}/password\`,
171 method: 'POST'
172 };
173 }
174
175 break;
176
177 case 'collection':
178 // Pagination links
179 if (resource.pagination) {
180 const { currentPage, totalPages, limit } = resource.pagination;
181 const baseQuery = resource.query || {};
182
183 links.self = {
184 href: this.buildUrl('/api/v1/products', { ...baseQuery, page: currentPage, limit })
185 };
186
187 if (currentPage > 1) {
188 links.first = {
189 href: this.buildUrl('/api/v1/products', { ...baseQuery, page: 1, limit })
190 };
191 links.prev = {
192 href: this.buildUrl('/api/v1/products', { ...baseQuery, page: currentPage - 1, limit })
193 };
194 }
195
196 if (currentPage < totalPages) {
197 links.next = {
198 href: this.buildUrl('/api/v1/products', { ...baseQuery, page: currentPage + 1, limit })
199 };
200 links.last = {
201 href: this.buildUrl('/api/v1/products', { ...baseQuery, page: totalPages, limit })
202 };
203 }
204 }
205
206 // Collection operations
207 if (userPermissions.includes('product:create')) {
208 links.create = {
209 href: \`\${this.baseUrl}/api/v1/products\`,
210 method: 'POST',
211 schema: \`\${this.baseUrl}/api/v1/schemas/product\`
212 };
213 links.bulkCreate = {
214 href: \`\${this.baseUrl}/api/v1/products/bulk\`,
215 method: 'POST',
216 title: 'Create multiple products'
217 };
218 }
219
220 // Search and filtering
221 links.search = {
222 href: \`\${this.baseUrl}/api/v1/products/search{?q,category,priceMin,priceMax}\`,
223 templated: true
224 };
225
226 break;
227 }
228
229 return links;
230 }
231
232 // Get available status transitions for orders
233 getOrderStatusTransitions(currentStatus, userPermissions) {
234 const transitions = [];
235
236 const statusMap = {
237 'pending': [
238 { action: 'confirm', title: 'Confirm Order', permission: 'order:confirm' },
239 { action: 'cancel', title: 'Cancel Order', permission: 'order:cancel', requiresConfirmation: true }
240 ],
241 'confirmed': [
242 { action: 'process', title: 'Start Processing', permission: 'order:process' },
243 { action: 'cancel', title: 'Cancel Order', permission: 'order:cancel', requiresConfirmation: true }
244 ],
245 'processing': [
246 { action: 'ship', title: 'Ship Order', permission: 'order:ship' },
247 { action: 'cancel', title: 'Cancel Order', permission: 'order:cancel', requiresConfirmation: true }
248 ],
249 'shipped': [
250 { action: 'deliver', title: 'Mark as Delivered', permission: 'order:deliver' }
251 ],
252 'delivered': [],
253 'cancelled': [
254 { action: 'reopen', title: 'Reopen Order', permission: 'order:reopen' }
255 ]
256 };
257
258 const availableTransitions = statusMap[currentStatus] || [];
259
260 return availableTransitions.filter(transition =>
261 userPermissions.includes(transition.permission)
262 );
263 }
264
265 // Generate embedded resources
266 generateEmbedded(resource, resourceType, embedOptions = {}) {
267 const embedded = {};
268
269 switch (resourceType) {
270 case 'product':
271 if (embedOptions.category && resource.category) {
272 embedded.category = {
273 ...resource.category,
274 _links: this.generateLinks(resource.category, 'category')
275 };
276 }
277
278 if (embedOptions.variants && resource.variants) {
279 embedded.variants = resource.variants.map(variant => ({
280 ...variant,
281 _links: this.generateLinks(variant, 'productVariant')
282 }));
283 }
284
285 if (embedOptions.reviews && resource.reviews) {
286 embedded.reviews = resource.reviews.map(review => ({
287 ...review,
288 _links: this.generateLinks(review, 'review')
289 }));
290 }
291
292 break;
293
294 case 'order':
295 if (embedOptions.customer && resource.customer) {
296 embedded.customer = {
297 ...resource.customer,
298 _links: this.generateLinks(resource.customer, 'user')
299 };
300 }
301
302 if (embedOptions.items && resource.items) {
303 embedded.items = resource.items.map(item => ({
304 ...item,
305 _links: this.generateLinks(item.product, 'product')
306 }));
307 }
308
309 break;
310 }
311
312 return Object.keys(embedded).length > 0 ? embedded : undefined;
313 }
314
315 // Add HATEOAS to resource
316 addHATEOAS(resource, resourceType, options = {}) {
317 const { userPermissions = [], embedOptions = {} } = options;
318
319 const hateoasResource = {
320 ...resource,
321 _links: this.generateLinks(resource, resourceType, userPermissions)
322 };
323
324 const embedded = this.generateEmbedded(resource, resourceType, embedOptions);
325 if (embedded) {
326 hateoasResource._embedded = embedded;
327 }
328
329 return hateoasResource;
330 }
331
332 // Add HATEOAS to collection
333 addHATEOASToCollection(collection, options = {}) {
334 const { userPermissions = [], embedOptions = {} } = options;
335
336 return {
337 ...collection,
338 _links: this.generateLinks(collection, 'collection', userPermissions),
339 data: collection.data.map(item =>
340 this.addHATEOAS(item, options.itemType || 'product', { userPermissions, embedOptions })
341 )
342 };
343 }
344
345 // Helper to build URLs with query parameters
346 buildUrl(path, params = {}) {
347 const url = new URL(path, this.baseUrl);
348 Object.entries(params).forEach(([key, value]) => {
349 if (value !== undefined && value !== null) {
350 url.searchParams.append(key, value.toString());
351 }
352 });
353 return url.toString();
354 }
355}
356
357module.exports = HATEOASGenerator;Understand and implement the four levels of REST API maturity.
1/*
2RICHARDSON MATURITY MODEL - REST API Evolution
3
4Level 0: The Swamp of POX (Plain Old XML/JSON)
5- Single endpoint, single HTTP method (usually POST)
6- All operations tunneled through one URL
7- Example: /api/endpoint with different action parameters
8
9Level 1: Resources
10- Multiple resources, but limited HTTP methods
11- Individual URIs for different resources
12- Still primarily using POST for everything
13
14Level 2: HTTP Verbs
15- Proper use of HTTP methods (GET, POST, PUT, DELETE)
16- Correct HTTP status codes
17- Resources + proper HTTP semantics
18
19Level 3: Hypermedia Controls (HATEOAS)
20- Self-discovering API through hypermedia links
21- State transitions driven by server responses
22- True REST implementation
23*/
24
25// LEVEL 0 EXAMPLE (Anti-pattern)
26class Level0API {
27 // ❌ Everything goes through one endpoint
28 async handleRequest(req, res) {
29 const { action, data } = req.body;
30
31 switch (action) {
32 case 'getUser':
33 // GET user logic
34 break;
35 case 'createUser':
36 // POST user logic
37 break;
38 case 'updateUser':
39 // PUT user logic
40 break;
41 case 'deleteUser':
42 // DELETE user logic
43 break;
44 }
45 }
46}
47
48// LEVEL 1 EXAMPLE
49class Level1API {
50 // ✅ Different resources have different endpoints
51 setupRoutes() {
52 // Users resource
53 app.post('/api/users', this.createUser);
54 app.post('/api/users/get', this.getUser);
55 app.post('/api/users/update', this.updateUser);
56 app.post('/api/users/delete', this.deleteUser);
57
58 // Orders resource
59 app.post('/api/orders', this.createOrder);
60 app.post('/api/orders/get', this.getOrder);
61 app.post('/api/orders/update', this.updateOrder);
62
63 // Products resource
64 app.post('/api/products', this.createProduct);
65 app.post('/api/products/get', this.getProduct);
66 }
67}
68
69// LEVEL 2 EXAMPLE
70class Level2API {
71 // ✅ Proper HTTP methods and status codes
72 setupRoutes() {
73 // Users resource with proper HTTP verbs
74 app.get('/api/users', this.getUsers); // 200 OK
75 app.get('/api/users/:id', this.getUser); // 200 OK or 404 Not Found
76 app.post('/api/users', this.createUser); // 201 Created
77 app.put('/api/users/:id', this.updateUser); // 200 OK or 404 Not Found
78 app.delete('/api/users/:id', this.deleteUser); // 204 No Content or 404 Not Found
79
80 // Proper status codes
81 app.use(this.handleErrors);
82 }
83
84 async createUser(req, res) {
85 try {
86 const user = await userService.create(req.body);
87 res.status(201).json(user); // ✅ 201 Created
88 } catch (error) {
89 if (error.type === 'VALIDATION_ERROR') {
90 res.status(422).json({ error: error.message }); // ✅ 422 Unprocessable Entity
91 } else {
92 res.status(500).json({ error: 'Internal Server Error' }); // ✅ 500 Internal Server Error
93 }
94 }
95 }
96
97 async getUser(req, res) {
98 const user = await userService.findById(req.params.id);
99 if (!user) {
100 return res.status(404).json({ error: 'User not found' }); // ✅ 404 Not Found
101 }
102 res.status(200).json(user); // ✅ 200 OK
103 }
104}
105
106// LEVEL 3 EXAMPLE (Full REST with HATEOAS)
107class Level3API {
108 constructor() {
109 this.hateoasGenerator = new HATEOASGenerator('https://api.example.com');
110 }
111
112 // ✅ Full REST with hypermedia controls
113 setupRoutes() {
114 app.get('/api/users', this.getUsers);
115 app.get('/api/users/:id', this.getUser);
116 app.post('/api/users', this.createUser);
117 app.put('/api/users/:id', this.updateUser);
118 app.delete('/api/users/:id', this.deleteUser);
119
120 // Add HATEOAS middleware
121 app.use(this.addHypermediaControls);
122 }
123
124 async getUser(req, res) {
125 try {
126 const user = await userService.findById(req.params.id);
127
128 if (!user) {
129 return res.status(404).json({
130 error: 'User not found',
131 _links: {
132 users: { href: '/api/users' },
133 search: {
134 href: '/api/users/search{?q}',
135 templated: true
136 }
137 }
138 });
139 }
140
141 // Add hypermedia controls
142 const userWithLinks = this.hateoasGenerator.addHATEOAS(user, 'user', {
143 userPermissions: req.user?.permissions || []
144 });
145
146 res.status(200).json(userWithLinks);
147
148 } catch (error) {
149 res.status(500).json({
150 error: 'Internal Server Error',
151 _links: {
152 home: { href: '/api' },
153 support: { href: '/api/support' }
154 }
155 });
156 }
157 }
158
159 async getUsers(req, res) {
160 try {
161 const { page = 1, limit = 20 } = req.query;
162 const result = await userService.findAll({ page, limit });
163
164 const response = {
165 users: result.users.map(user =>
166 this.hateoasGenerator.addHATEOAS(user, 'user', {
167 userPermissions: req.user?.permissions || []
168 })
169 ),
170 pagination: result.pagination,
171 _links: {
172 self: { href: \`/api/users?page=\${page}&limit=\${limit}\` },
173 first: { href: \`/api/users?page=1&limit=\${limit}\` },
174 last: { href: \`/api/users?page=\${result.pagination.totalPages}&limit=\${limit}\` }
175 }
176 };
177
178 // Add conditional navigation links
179 if (result.pagination.hasNextPage) {
180 response._links.next = { href: \`/api/users?page=\${page + 1}&limit=\${limit}\` };
181 }
182
183 if (result.pagination.hasPrevPage) {
184 response._links.prev = { href: \`/api/users?page=\${page - 1}&limit=\${limit}\` };
185 }
186
187 // Add action links based on permissions
188 if (req.user?.permissions.includes('user:create')) {
189 response._links.create = {
190 href: '/api/users',
191 method: 'POST',
192 title: 'Create a new user'
193 };
194 }
195
196 res.status(200).json(response);
197
198 } catch (error) {
199 res.status(500).json({
200 error: 'Internal Server Error',
201 _links: {
202 home: { href: '/api' },
203 docs: { href: '/api/docs' }
204 }
205 });
206 }
207 }
208
209 async createUser(req, res) {
210 try {
211 const user = await userService.create(req.body);
212
213 const userWithLinks = this.hateoasGenerator.addHATEOAS(user, 'user', {
214 userPermissions: req.user?.permissions || []
215 });
216
217 // Set Location header
218 res.set('Location', \`/api/users/\${user.id}\`);
219
220 res.status(201).json({
221 ...userWithLinks,
222 _links: {
223 ...userWithLinks._links,
224 // Add specific next actions
225 verify: {
226 href: \`/api/users/\${user.id}/verify\`,
227 method: 'POST',
228 title: 'Verify user email'
229 },
230 welcome: {
231 href: \`/api/users/\${user.id}/welcome\`,
232 method: 'POST',
233 title: 'Send welcome email'
234 }
235 }
236 });
237
238 } catch (error) {
239 if (error.type === 'VALIDATION_ERROR') {
240 res.status(422).json({
241 error: 'Validation Error',
242 details: error.details,
243 _links: {
244 retry: {
245 href: '/api/users',
246 method: 'POST',
247 title: 'Try creating user again'
248 },
249 docs: {
250 href: '/api/docs/users#create',
251 title: 'User creation documentation'
252 }
253 }
254 });
255 } else {
256 res.status(500).json({
257 error: 'Internal Server Error',
258 _links: {
259 support: { href: '/api/support' },
260 home: { href: '/api' }
261 }
262 });
263 }
264 }
265 }
266
267 // Middleware to add common hypermedia controls
268 addHypermediaControls(req, res, next) {
269 const originalJson = res.json;
270
271 res.json = function(data) {
272 // Add common links to all responses
273 if (data && typeof data === 'object' && !data._links) {
274 data._links = {
275 ...data._links,
276 home: { href: '/api' },
277 docs: { href: '/api/docs' }
278 };
279 }
280
281 return originalJson.call(this, data);
282 };
283
284 next();
285 }
286}
287
288// Example of state transitions with HATEOAS
289class OrderStateMachine {
290 getAvailableActions(order, userPermissions) {
291 const actions = [];
292
293 switch (order.status) {
294 case 'pending':
295 if (userPermissions.includes('order:confirm')) {
296 actions.push({
297 rel: 'confirm',
298 href: \`/api/orders/\${order.id}/confirm\`,
299 method: 'POST',
300 title: 'Confirm this order'
301 });
302 }
303
304 if (userPermissions.includes('order:cancel')) {
305 actions.push({
306 rel: 'cancel',
307 href: \`/api/orders/\${order.id}/cancel\`,
308 method: 'POST',
309 title: 'Cancel this order',
310 confirmationRequired: true
311 });
312 }
313 break;
314
315 case 'confirmed':
316 if (userPermissions.includes('order:ship')) {
317 actions.push({
318 rel: 'ship',
319 href: \`/api/orders/\${order.id}/ship\`,
320 method: 'POST',
321 title: 'Ship this order',
322 schema: '/api/schemas/ship-order'
323 });
324 }
325 break;
326
327 case 'shipped':
328 actions.push({
329 rel: 'track',
330 href: \`/api/orders/\${order.id}/tracking\`,
331 method: 'GET',
332 title: 'Track this shipment'
333 });
334
335 if (userPermissions.includes('order:deliver')) {
336 actions.push({
337 rel: 'deliver',
338 href: \`/api/orders/\${order.id}/deliver\`,
339 method: 'POST',
340 title: 'Mark as delivered'
341 });
342 }
343 break;
344 }
345
346 return actions;
347 }
348}
349
350module.exports = {
351 Level0API,
352 Level1API,
353 Level2API,
354 Level3API,
355 OrderStateMachine
356};Learn about Practice Exercises.
Exercise 1: Complete REST API for Task Management
Design and implement a full RESTful API for task management with proper resource modeling, HTTP methods, status codes, and HATEOAS implementation.
Exercise 2: API Versioning Implementation
Implement multiple API versions with backward compatibility, deprecation warnings, and smooth migration paths for clients.
Exercise 3: Resource Relationship Modeling
Model complex resource relationships in a blog system with posts, comments, categories, and tags using proper REST principles.
REST Principles:
Follow the six architectural constraints for scalable APIs
Resource Design:
Use intuitive naming conventions and hierarchical structures
HTTP Semantics:
Apply proper methods and status codes for different operations
API Versioning:
Implement versioning strategies for backward compatibility
Create self-describing APIs with hypermedia controls
Maturity Model:
Evolve APIs through Richardson Maturity Model levels
You've mastered REST API design principles! You can now create professional, scalable, and maintainable RESTful APIs following industry standards and best practices.
Your progress is saved in this browser, on this device only.