Building REST APIs with Node.js
What is a REST API?
REST (Representational State Transfer) is an architectural style for designing networked applications. A REST API uses HTTP requests to GET, PUT, POST, and DELETE data, making it the standard for building web services.
REST Principles
- Stateless: Each request contains all information needed to understand and process it
- Client-Server: Separation of concerns between client and server
- Cacheable: Responses should indicate if they can be cached
- Uniform Interface: Standardized methods and conventions
- Layered System: Architecture composed of hierarchical layers
Setting Up the Project
mkdir nodejs-rest-api
cd nodejs-rest-api
npm init -y
npm install express mongoose cors helmet morgan dotenv
API Design Best Practices
Use Proper HTTP Methods
- GET: Retrieve resources
- POST: Create new resources
- PUT: Update entire resources
- PATCH: Partial updates
- DELETE: Remove resources
Use Nouns, Not Verbs
// Good
GET /users
POST /users
PUT /users/123
DELETE /users/123
// Bad
GET /getAllUsers
POST /createUser
PUT /updateUser/123
Use Plural Nouns for Collections
/users // Collection of users
/users/123 // Specific user
/users/123/posts // Posts by specific user
Building the API
Basic Express Setup
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const app = express();
// Middleware
app.use(helmet()); // Security headers
app.use(cors()); // Enable CORS
app.use(morgan('combined')); // Logging
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
Database Model
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
age: Number,
createdAt: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', userSchema);
Controller Functions
// Get all users
exports.getAllUsers = async (req, res) => {
try {
const users = await User.find();
res.status(200).json({
status: 'success',
results: users.length,
data: { users }
});
} catch (error) {
res.status(500).json({
status: 'error',
message: error.message
});
}
};
// Get user by ID
exports.getUserById = async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
status: 'fail',
message: 'User not found'
});
}
res.status(200).json({
status: 'success',
data: { user }
});
} catch (error) {
res.status(500).json({
status: 'error',
message: error.message
});
}
};
// Create user
exports.createUser = async (req, res) => {
try {
const newUser = await User.create(req.body);
res.status(201).json({
status: 'success',
data: { user: newUser }
});
} catch (error) {
res.status(400).json({
status: 'fail',
message: error.message
});
}
};
// Update user
exports.updateUser = async (req, res) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!user) {
return res.status(404).json({
status: 'fail',
message: 'User not found'
});
}
res.status(200).json({
status: 'success',
data: { user }
});
} catch (error) {
res.status(400).json({
status: 'fail',
message: error.message
});
}
};
// Delete user
exports.deleteUser = async (req, res) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
status: 'fail',
message: 'User not found'
});
}
res.status(204).json({
status: 'success',
data: null
});
} catch (error) {
res.status(500).json({
status: 'error',
message: error.message
});
}
};
Routes
const express = require('express');
const userController = require('./userController');
const router = express.Router();
// GET /api/users
router.get('/', userController.getAllUsers);
// GET /api/users/:id
router.get('/:id', userController.getUserById);
// POST /api/users
router.post('/', userController.createUser);
// PUT /api/users/:id
router.put('/:id', userController.updateUser);
// DELETE /api/users/:id
router.delete('/:id', userController.deleteUser);
module.exports = router;
Main App File
const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./routes/userRoutes');
const app = express();
// Connect to MongoDB
mongoose.connect(process.env.DATABASE_URL, {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Routes
app.use('/api/users', userRoutes);
// Error handling middleware
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Advanced Features
Input Validation
const { body, validationResult } = require('express-validator');
const validateUser = [
body('name').isLength({ min: 2 }).withMessage('Name must be at least 2 characters'),
body('email').isEmail().withMessage('Please provide a valid email'),
body('age').isInt({ min: 0, max: 120 }).withMessage('Age must be between 0 and 120')
];
// Use in route
router.post('/', validateUser, userController.createUser);
Response Format Standardization
// Success response format
{
"status": "success",
"data": {
"user": { ... }
}
}
// Error response format
{
"status": "error",
"message": "User not found"
}
Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
max: 100, // limit each IP to 100 requests per windowMs
windowMs: 15 * 60 * 1000, // 15 minutes
message: 'Too many requests from this IP, please try again later.'
});
app.use('/api/', limiter);
API Documentation
Consider using Swagger/OpenAPI for API documentation:
npm install swagger-ui-express swagger-jsdoc
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Node.js REST API',
version: '1.0.0',
},
},
apis: ['./routes/*.js'],
};
const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
Testing the API
Using curl
# Get all users
curl http://localhost:3000/api/users
# Create a user
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com","age":30}'
# Get user by ID
curl http://localhost:3000/api/users/1
# Update user
curl -X PUT http://localhost:3000/api/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe"}'
# Delete user
curl -X DELETE http://localhost:3000/api/users/1
Using Postman
Import the following collection into Postman:
{
"info": {
"name": "Node.js REST API",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Get All Users",
"request": {
"method": "GET",
"url": "http://localhost:3000/api/users"
}
},
{
"name": "Create User",
"request": {
"method": "POST",
"url": "http://localhost:3000/api/users",
"body": {
"mode": "raw",
"raw": "{\"name\":\"John Doe\",\"email\":\"john@example.com\",\"age\":30}"
}
}
}
]
}
Best Practices Summary
- Use proper HTTP methods and status codes
- Implement proper error handling
- Validate input data
- Use consistent response formats
- Implement rate limiting
- Add proper logging
- Write comprehensive tests
- Document your API
Next Steps
- Add authentication and authorization
- Write comprehensive API tests
- Deploy your API to production
- Optimize API performance
Conclusion
Building REST APIs with Node.js and Express provides a solid foundation for modern web applications. By following these best practices, you can create robust, scalable, and maintainable APIs that serve your applications effectively.