Building REST APIs with Node.js
REST APIs are the foundation of modern web applications. This article covers REST API development with Node.js.
Introduction
REST APIs provide:
- Standardized endpoints
- HTTP methods
- Status codes
- Request/response formats
- Scalability
Understanding REST APIs helps you:
- Build web services
- Create scalable backends
- Integrate with frontends
- Design clean APIs
- Handle data exchange
REST API Fundamentals
HTTP Methods and Status Codes
const express = require('express');
const app = express();
app.use(express.json());
// โ
Good: GET - Retrieve resources
app.get('/api/users', (req, res) => {
res.status(200).json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
});
// โ
Good: GET - Retrieve single resource
app.get('/api/users/:id', (req, res) => {
const user = { id: req.params.id, name: 'John' };
res.status(200).json(user);
});
// โ
Good: POST - Create resource
app.post('/api/users', (req, res) => {
const newUser = { id: 3, ...req.body };
res.status(201).json(newUser);
});
// โ
Good: PUT - Update resource
app.put('/api/users/:id', (req, res) => {
const updatedUser = { id: req.params.id, ...req.body };
res.status(200).json(updatedUser);
});
// โ
Good: PATCH - Partial update
app.patch('/api/users/:id', (req, res) => {
const user = { id: req.params.id, name: 'John', ...req.body };
res.status(200).json(user);
});
// โ
Good: DELETE - Remove resource
app.delete('/api/users/:id', (req, res) => {
res.status(204).send();
});
// โ
Good: Status codes
// 200 OK - Success
// 201 Created - Resource created
// 204 No Content - Success, no content
// 400 Bad Request - Invalid input
// 401 Unauthorized - Authentication required
// 403 Forbidden - Access denied
// 404 Not Found - Resource not found
// 500 Internal Server Error - Server error
API Response Format
// โ
Good: Consistent response format
const sendResponse = (res, status, data, message) => {
res.status(status).json({
success: status < 400,
status,
message,
data
});
};
app.get('/api/users', (req, res) => {
const users = [{ id: 1, name: 'John' }];
sendResponse(res, 200, users, 'Users retrieved successfully');
});
app.post('/api/users', (req, res) => {
const newUser = { id: 1, ...req.body };
sendResponse(res, 201, newUser, 'User created successfully');
});
// โ
Good: Error response format
app.get('/api/users/:id', (req, res) => {
const user = null;
if (!user) {
return sendResponse(res, 404, null, 'User not found');
}
sendResponse(res, 200, user, 'User retrieved successfully');
});
// โ
Good: Pagination response
app.get('/api/users', (req, res) => {
const page = req.query.page || 1;
const limit = req.query.limit || 10;
const users = [{ id: 1, name: 'John' }];
res.json({
success: true,
data: users,
pagination: {
page,
limit,
total: 100,
pages: Math.ceil(100 / limit)
}
});
});
API Design Patterns
RESTful Endpoints
const express = require('express');
const app = express();
app.use(express.json());
// โ
Good: Resource-based endpoints
// Users
app.get('/api/users', (req, res) => { /* List users */ });
app.post('/api/users', (req, res) => { /* Create user */ });
app.get('/api/users/:id', (req, res) => { /* Get user */ });
app.put('/api/users/:id', (req, res) => { /* Update user */ });
app.delete('/api/users/:id', (req, res) => { /* Delete user */ });
// Posts
app.get('/api/posts', (req, res) => { /* List posts */ });
app.post('/api/posts', (req, res) => { /* Create post */ });
app.get('/api/posts/:id', (req, res) => { /* Get post */ });
app.put('/api/posts/:id', (req, res) => { /* Update post */ });
app.delete('/api/posts/:id', (req, res) => { /* Delete post */ });
// โ
Good: Nested resources
app.get('/api/users/:userId/posts', (req, res) => {
const userId = req.params.userId;
res.json([{ id: 1, userId, title: 'Post 1' }]);
});
app.post('/api/users/:userId/posts', (req, res) => {
const userId = req.params.userId;
res.status(201).json({ id: 1, userId, ...req.body });
});
// โ
Good: Query parameters for filtering
app.get('/api/users', (req, res) => {
const { role, status, page = 1, limit = 10 } = req.query;
// Filter users based on query parameters
res.json([]);
});
// โ
Good: Sorting and pagination
app.get('/api/users', (req, res) => {
const { sort = 'createdAt', order = 'desc', page = 1, limit = 10 } = req.query;
const offset = (page - 1) * limit;
// Apply sorting and pagination
res.json([]);
});
API Versioning
const express = require('express');
const app = express();
// โ
Good: URL versioning
app.get('/api/v1/users', (req, res) => {
res.json([{ id: 1, name: 'John' }]);
});
app.get('/api/v2/users', (req, res) => {
res.json([{ id: 1, name: 'John', email: '[email protected]' }]);
});
// โ
Good: Header versioning
app.get('/api/users', (req, res) => {
const version = req.headers['api-version'] || '1';
if (version === '2') {
res.json([{ id: 1, name: 'John', email: '[email protected]' }]);
} else {
res.json([{ id: 1, name: 'John' }]);
}
});
// โ
Good: Router versioning
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users', (req, res) => {
res.json([{ id: 1, name: 'John' }]);
});
v2Router.get('/users', (req, res) => {
res.json([{ id: 1, name: 'John', email: '[email protected]' }]);
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Complete API Example
User API Implementation
// controllers/userController.js
const User = require('../models/User');
const getUsers = async (req, res, next) => {
try {
const { page = 1, limit = 10, role } = req.query;
const offset = (page - 1) * limit;
let query = {};
if (role) query.role = role;
const users = await User.find(query)
.limit(limit)
.skip(offset);
const total = await User.countDocuments(query);
res.json({
success: true,
data: users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (err) {
next(err);
}
};
const getUserById = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({ success: true, data: user });
} catch (err) {
next(err);
}
};
const createUser = async (req, res, next) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json({ success: true, data: user });
} catch (err) {
next(err);
}
};
const updateUser = async (req, res, next) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({ success: true, data: user });
} catch (err) {
next(err);
}
};
const deleteUser = async (req, res, next) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.status(204).send();
} catch (err) {
next(err);
}
};
module.exports = {
getUsers,
getUserById,
createUser,
updateUser,
deleteUser
};
// routes/users.js
const express = require('express');
const { body } = require('express-validator');
const validate = require('../middleware/validate');
const authenticate = require('../middleware/auth');
const userController = require('../controllers/userController');
const router = express.Router();
router.get('/', userController.getUsers);
router.post(
'/',
[
body('name').notEmpty(),
body('email').isEmail(),
body('password').isLength({ min: 8 })
],
validate,
userController.createUser
);
router.get('/:id', userController.getUserById);
router.put(
'/:id',
authenticate,
[
body('name').optional().notEmpty(),
body('email').optional().isEmail()
],
validate,
userController.updateUser
);
router.delete('/:id', authenticate, userController.deleteUser);
module.exports = router;
// app.js
const express = require('express');
const userRoutes = require('./routes/users');
const errorHandler = require('./middleware/errorHandler');
const app = express();
app.use(express.json());
app.use('/api/users', userRoutes);
app.use(errorHandler);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
API Documentation
OpenAPI/Swagger Documentation
// โ
Good: Swagger setup
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'
},
servers: [
{ url: 'http://localhost:3000' }
]
},
apis: ['./routes/*.js']
};
const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
// โ
Good: Document endpoints
/**
* @swagger
* /api/users:
* get:
* summary: Get all users
* responses:
* 200:
* description: List of users
*/
app.get('/api/users', (req, res) => {
res.json([]);
});
Best Practices
-
Use consistent naming:
// โ Good: Consistent naming app.get('/api/users', ...); app.get('/api/posts', ...); // โ Bad: Inconsistent naming app.get('/api/users', ...); app.get('/api/get-posts', ...); -
Use proper status codes:
// โ Good: Proper status codes res.status(201).json(newUser); // Created res.status(204).send(); // No content res.status(400).json(error); // Bad request // โ Bad: Always 200 res.json(newUser); -
Validate input:
// โ Good: Validate input app.post('/api/users', validate, (req, res) => { // Process validated data }); // โ Bad: No validation app.post('/api/users', (req, res) => { // Process unvalidated data });
Summary
REST APIs are essential. Key takeaways:
- Use HTTP methods correctly
- Design resource-based endpoints
- Use proper status codes
- Implement pagination and filtering
- Version your APIs
- Validate input
- Document endpoints
- Handle errors properly
Related Resources
- REST API Best Practices
- HTTP Status Codes
- OpenAPI Specification
- Swagger Documentation
- API Design Guide
Next Steps
- Learn about Authentication
- Explore Error Handling
- Study Deployment
- Practice API development
- Build production-ready APIs
Comments