REST API Best Practices for Backend Engineers
REST API Best Practices for Backend Engineers
Building robust, scalable REST APIs requires following established conventions and best practices. This guide covers everything you need to know to create production-ready APIs.
URL Design
Use Nouns, Not Verbs
# Good GET /api/users POST /api/users PUT /api/users/123 DELETE /api/users/123 # Bad GET /api/getUsers POST /api/createUser POST /api/updateUser POST /api/deleteUser
Use Plural Nouns
# Good GET /api/users GET /api/orders GET /api/products # Bad GET /api/user GET /api/order GET /api/product
Use Hierarchical Structure
# Good GET /api/users/123/orders GET /api/users/123/orders/456 # Bad GET /api/userOrders?userId=123 GET /api/orderDetails?orderId=456
HTTP Status Codes
Success Codes
// 200 OK - Successful GET, PUT, PATCH app.get('/api/users/:id', (req, res) => { const user = getUserById(req.params.id); res.status(200).json(user); }); // 201 Created - Successful POST app.post('/api/users', (req, res) => { const user = createUser(req.body); res.status(201).json(user); }); // 204 No Content - Successful DELETE app.delete('/api/users/:id', (req, res) => { deleteUser(req.params.id); res.status(204).send(); });
Java/Spring Boot:
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; // 200 OK - Successful GET, PUT, PATCH @GetMapping("/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { User user = userService.findById(id); return ResponseEntity.ok(user); } // 201 Created - Successful POST @PostMapping public ResponseEntity<User> createUser(@Valid @RequestBody User user) { User createdUser = userService.save(user); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } // 204 No Content - Successful DELETE @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { userService.deleteById(id); return ResponseEntity.noContent().build(); } }
Error Codes
// 400 Bad Request - Invalid input app.post('/api/users', (req, res) => { if (!req.body.email) { return res.status(400).json({ error: 'Email is required' }); } }); // 401 Unauthorized - Authentication required app.get('/api/profile', authenticateToken, (req, res) => { res.json(req.user); }); // 403 Forbidden - Access denied app.delete('/api/users/:id', (req, res) => { if (req.user.id !== req.params.id) { return res.status(403).json({ error: 'Access denied' }); } }); // 404 Not Found - Resource doesn't exist app.get('/api/users/:id', (req, res) => { const user = getUserById(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); }); // 422 Unprocessable Entity - Validation failed app.post('/api/users', (req, res) => { const errors = validateUser(req.body); if (errors.length > 0) { return res.status(422).json({ error: 'Validation failed', details: errors }); } }); // 500 Internal Server Error - Server error app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Internal server error' }); });
Java/Spring Boot:
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; // 400 Bad Request - Invalid input @PostMapping public ResponseEntity<?> createUser(@Valid @RequestBody User user, BindingResult result) { if (result.hasErrors()) { return ResponseEntity.badRequest() .body(new ErrorResponse("Validation failed", result.getFieldErrors())); } User createdUser = userService.save(user); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } // 401 Unauthorized - Authentication required @GetMapping("/profile") @PreAuthorize("isAuthenticated()") public ResponseEntity<User> getProfile(Authentication auth) { User user = userService.findByEmail(auth.getName()); return ResponseEntity.ok(user); } // 403 Forbidden - Access denied @DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN') or @userService.isOwner(#id, authentication.name)") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { userService.deleteById(id); return ResponseEntity.noContent().build(); } // 404 Not Found - Resource doesn't exist @GetMapping("/{id}") public ResponseEntity<User> getUser(@PathVariable Long id) { return userService.findById(id) .map(user -> ResponseEntity.ok(user)) .orElse(ResponseEntity.notFound().build()); } // 422 Unprocessable Entity - Validation failed @PostMapping public ResponseEntity<?> createUserWithValidation(@Valid @RequestBody User user, BindingResult result) { if (result.hasErrors()) { return ResponseEntity.unprocessableEntity() .body(new ValidationErrorResponse("Validation failed", result.getFieldErrors())); } User createdUser = userService.save(user); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } } // Global Exception Handler @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleInternalServerError(Exception ex) { log.error("Internal server error", ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ErrorResponse("Internal server error", null)); } }
Request/Response Format
Consistent Response Structure
// Success Response { "success": true, "data": { "id": 123, "name": "John Doe", "email": "john@example.com" }, "message": "User created successfully" } // Error Response { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Email is required", "details": [ { "field": "email", "message": "Email is required" } ] } }
Pagination
// Request GET /api/users?page=1&limit=10&sort=name&order=asc // Response { "success": true, "data": [ // ... users array ], "pagination": { "page": 1, "limit": 10, "total": 100, "pages": 10, "hasNext": true, "hasPrev": false } }
Authentication & Authorization
JWT Implementation
const jwt = require('jsonwebtoken'); // Generate token function generateToken(user) { return jwt.sign( { id: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET, { expiresIn: '24h' } ); } // Verify token function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Access token required' }); } jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ error: 'Invalid token' }); } req.user = user; next(); }); }
API Versioning
URL Versioning
// Version 1 app.use('/api/v1/users', userRoutesV1); // Version 2 app.use('/api/v2/users', userRoutesV2);
Header Versioning
app.use((req, res, next) => { const version = req.headers['api-version'] || '1.0'; req.apiVersion = version; next(); });
Rate Limiting
const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests from this IP, please try again later.' } }); app.use('/api/', limiter);
Input Validation
const { body, validationResult } = require('express-validator'); const validateUser = [ body('email').isEmail().normalizeEmail(), body('password').isLength({ min: 6 }), body('name').trim().isLength({ min: 1 }), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ error: 'Validation failed', details: errors.array() }); } next(); } ]; app.post('/api/users', validateUser, createUser);
Error Handling
class APIError extends Error { constructor(message, statusCode, code) { super(message); this.statusCode = statusCode; this.code = code; this.isOperational = true; } } // Global error handler app.use((err, req, res, next) => { if (err instanceof APIError) { return res.status(err.statusCode).json({ success: false, error: { code: err.code, message: err.message } }); } // Log unexpected errors console.error(err.stack); res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } }); });
Performance Optimization
Caching
const redis = require('redis'); const client = redis.createClient(); async function getCachedUser(userId) { const cached = await client.get(`user:${userId}`); if (cached) { return JSON.parse(cached); } const user = await getUserFromDB(userId); await client.setex(`user:${userId}`, 3600, JSON.stringify(user)); return user; }
Compression
const compression = require('compression'); app.use(compression());
Documentation
OpenAPI/Swagger
const swaggerJsdoc = require('swagger-jsdoc'); const swaggerUi = require('swagger-ui-express'); const options = { definition: { openapi: '3.0.0', info: { title: 'User API', version: '1.0.0', description: 'A simple User API' }, servers: [ { url: 'http://localhost:3000/api', description: 'Development server' } ] }, apis: ['./routes/*.js'] }; const specs = swaggerJsdoc(options); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
Testing
const request = require('supertest'); const app = require('../app'); describe('User API', () => { test('GET /api/users should return users', async () => { const response = await request(app) .get('/api/users') .expect(200); expect(response.body.success).toBe(true); expect(Array.isArray(response.body.data)).toBe(true); }); test('POST /api/users should create user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', password: 'password123' }; const response = await request(app) .post('/api/users') .send(userData) .expect(201); expect(response.body.success).toBe(true); expect(response.body.data.email).toBe(userData.email); }); });
Conclusion
Following these REST API best practices will help you build robust, scalable, and maintainable APIs. Remember to:
- Keep your APIs consistent
- Use appropriate HTTP status codes
- Implement proper error handling
- Add authentication and authorization
- Document your APIs
- Test thoroughly
- Monitor and optimize performance
These practices will make your APIs more reliable and easier to use for frontend developers and API consumers.
Related Articles
Incident Playbook for Beginners: Real-World Monitoring and Troubleshooting Stories
A story-driven, plain English incident playbook for new backend & SRE engineers. Find, fix, and prevent outages with empathy and practical steps.
System Design Power-Guide 2025: What To Learn, In What Order, With Real-World Links
Stop bookmarking random threads. This is a tight, no-fluff map of what to study for system design in 2025 - what each topic is, why it matters in interviews and production, and where to go deeper.
DSA Patterns Master Guide: How To Identify Problems, Pick Patterns, and Practice (With LeetCode Sets)
A practical, pattern-first road map for entry-level engineers. Learn how to identify the right pattern quickly, apply a small algorithm template, know variants and pitfalls, and practice with curated LeetCode problems.