Building a RESTful API is a fundamental skill for modern backend development. Node.js with Express provides a fast, minimalist framework perfect for creating scalable APIs. This comprehensive guide will take you from zero to a production-ready API with authentication, database integration, and best practices.
What is a REST API?
REST (Representational State Transfer) is an architectural style for designing networked applications. A REST API uses HTTP methods to perform CRUD operations:
- GET: Retrieve data
- POST: Create new data
- PUT/PATCH: Update existing data
- DELETE: Remove data
REST Principles
REST APIs follow these core principles[1]:
- Stateless: Each request contains all necessary information
- Client-Server Architecture: Separation of concerns
- Cacheable: Responses indicate if they can be cached
- Uniform Interface: Consistent naming and structure
- Layered System: Intermediate servers (proxies, load balancers) are transparent
Project Setup
Step 1: Initialize Project
mkdir my-rest-api
cd my-rest-api
npm init -y
Step 2: Install Dependencies
# Core dependencies
npm install express dotenv
## Database (we'll use MongoDB)
npm install mongoose
## Middleware
npm install cors helmet express-rate-limit
## Validation
npm install express-validator
## Development dependencies
npm install --save-dev nodemon
Step 3: Project Structure
Create this organized structure:
my-rest-api/
├── src/
│ ├── config/
│ │ └── database.js
│ ├── controllers/
│ │ └── userController.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ └── userRoutes.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── errorHandler.js
│ └── app.js
├── .env
├── .gitignore
├── package.json
└── server.js
Building the API
Step 1: Configure Environment Variables
Create .env file:
## Server Configuration
PORT=3000
NODE_ENV=development
## Database
MONGODB_URI=mongodb://localhost:27017/myapi
MONGODB_TEST_URI=mongodb://localhost:27017/myapi-test
## JWT (for authentication)
JWT_SECRET=your-super-secret-jwt-key-change-in-production
JWT_EXPIRES_IN=24h
Create .gitignore:
node_modules/
.env
*.log
dist/
coverage/
Step 2: Database Connection
// src/config/database.js
const mongoose = require('mongoose');
const connectDatabase = async () => {
try {
const options = {
useNewUrlParser: true,
useUnifiedTopology: true,
};
const conn = await mongoose.connect(
process.env.MONGODB_URI,
options
);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDatabase;
Step 3: Create Data Model
// src/models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
maxlength: [50, 'Name cannot exceed 50 characters']
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
match: [
/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
'Please provide a valid email'
]
},
age: {
type: Number,
min: [0, 'Age must be positive'],
max: [120, 'Age must be realistic']
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true // Adds createdAt and updatedAt
});
// Index for faster queries
userSchema.index({ email: 1 });
// Virtual property (computed, not stored in DB)
userSchema.virtual('profile').get(function() {
return {
name: this.name,
email: this.email,
role: this.role
};
});
// Instance method
userSchema.methods.toJSON = function() {
const user = this.toObject();
delete user.__v;
return user;
};
module.exports = mongoose.model('User', userSchema);
Step 4: Create Controller
// src/controllers/userController.js
const User = require('../models/User');
const { validationResult } = require('express-validator');
// @desc Get all users
// @route GET /api/users
// @access Public
exports.getUsers = async (req, res, next) => {
try {
// Pagination
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const skip = (page - 1) * limit;
// Filtering
const filter = {};
if (req.query.role) filter.role = req.query.role;
if (req.query.isActive) filter.isActive = req.query.isActive === 'true';
// Sorting
const sort = {};
if (req.query.sortBy) {
const parts = req.query.sortBy.split(':');
sort[parts[0]] = parts[1] === 'desc' ? -1 : 1;
}
const users = await User.find(filter)
.sort(sort)
.limit(limit)
.skip(skip)
.select('-__v');
const total = await User.countDocuments(filter);
res.status(200).json({
success: true,
count: users.length,
total,
page,
pages: Math.ceil(total / limit),
data: users
});
} catch (error) {
next(error);
}
};
// @desc Get single user
// @route GET /api/users/:id
// @access Public
exports.getUser = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
next(error);
}
};
// @desc Create user
// @route POST /api/users
// @access Public
exports.createUser = async (req, res, next) => {
try {
// Validate input
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
const user = await User.create(req.body);
res.status(201).json({
success: true,
data: user
});
} catch (error) {
// Handle duplicate key error
if (error.code === 11000) {
return res.status(400).json({
success: false,
error: 'Email already exists'
});
}
next(error);
}
};
// @desc Update user
// @route PUT /api/users/:id
// @access Public
exports.updateUser = async (req, res, next) => {
try {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{
new: true, // Return updated document
runValidators: true // Run model validators
}
);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.status(200).json({
success: true,
data: user
});
} catch (error) {
next(error);
}
};
// @desc Delete user
// @route DELETE /api/users/:id
// @access Public
exports.deleteUser = async (req, res, next) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
res.status(200).json({
success: true,
data: {},
message: 'User deleted successfully'
});
} catch (error) {
next(error);
}
};
Step 5: Define Routes
// src/routes/userRoutes.js
const express = require('express');
const { body } = require('express-validator');
const {
getUsers,
getUser,
createUser,
updateUser,
deleteUser
} = require('../controllers/userController');
const router = express.Router();
// Validation middleware
const userValidation = [
body('name')
.trim()
.notEmpty().withMessage('Name is required')
.isLength({ max: 50 }).withMessage('Name too long'),
body('email')
.isEmail().withMessage('Valid email is required')
.normalizeEmail(),
body('age')
.optional()
.isInt({ min: 0, max: 120 }).withMessage('Age must be between 0 and 120')
];
// Routes
router.route('/')
.get(getUsers)
.post(userValidation, createUser);
router.route('/:id')
.get(getUser)
.put(updateUser)
.delete(deleteUser);
module.exports = router;
Step 6: Create Middleware
// src/middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// Log error for debugging
console.error(err);
// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = { message, statusCode: 404 };
}
// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = { message, statusCode: 400 };
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors)
.map(val => val.message)
.join(', ');
error = { message, statusCode: 400 };
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error'
});
};
module.exports = errorHandler;
Step 7: Configure Express App
// src/app.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const errorHandler = require('./middleware/errorHandler');
const connectDatabase = require('./config/database');
const app = express();
// Connect to database
connectDatabase();
// Security middleware
app.use(helmet()); // Set security headers
app.use(cors()); // Enable CORS
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later.'
});
app.use('/api/', limiter);
// Body parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Request logging (simple)
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
// Routes
app.use('/api/users', require('./routes/userRoutes'));
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Route not found'
});
});
// Error handler (must be last)
app.use(errorHandler);
module.exports = app;
Step 8: Create Server Entry Point
// server.js
const app = require('./src/app');
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.log(`Error: ${err.message}`);
server.close(() => process.exit(1));
});
Step 9: Update package.json Scripts
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "NODE_ENV=test jest --coverage",
"lint": "eslint src/**/*.js"
}
}
Testing Your API
Start the Server
npm run dev
Test with cURL
Create a user:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "[email protected]",
"age": 30
}'
Get all users:
curl http://localhost:3000/api/users
Get single user:
curl http://localhost:3000/api/users/507f1f77bcf86cd799439011
Update user:
curl -X PUT http://localhost:3000/api/users/507f1f77bcf86cd799439011 \
-H "Content-Type: application/json" \
-d '{"age": 31}'
Delete user:
curl -X DELETE http://localhost:3000/api/users/507f1f77bcf86cd799439011
Query Parameters
## Pagination
curl "http://localhost:3000/api/users?page=2&limit=5"
## Filtering
curl "http://localhost:3000/api/users?role=admin&isActive=true"
## Sorting
curl "http://localhost:3000/api/users?sortBy=createdAt:desc"
Advanced Features
1. Add Authentication
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
exports.protect = async (req, res, next) => {
let token;
if (req.headers.authorization?.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
success: false,
error: 'Not authorized to access this route'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({
success: false,
error: 'Not authorized'
});
}
};
Use in routes:
const { protect } = require('../middleware/auth');
router.delete('/:id', protect, deleteUser);
2. Add Request Logging
npm install morgan
const morgan = require('morgan');
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
3. Add API Documentation
npm install swagger-ui-express swagger-jsdoc
// Add to app.js
const swaggerUi = require('swagger-ui-express');
const swaggerJsDoc = require('swagger-jsdoc');
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'My REST API',
version: '1.0.0',
description: 'A simple REST API built with Express'
},
servers: [
{ url: 'http://localhost:3000' }
]
},
apis: ['./src/routes/*.js']
};
const swaggerDocs = swaggerJsDoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
Best Practices Checklist
- Use environment variables for configuration
- Implement proper error handling
- Add input validation
- Use appropriate HTTP status codes
- Implement pagination for list endpoints
- Add rate limiting
- Use security middleware (helmet, cors)
- Log requests and errors
- Version your API (
/api/v1/users) - Document your API
- Write tests
- Use HTTPS in production
HTTP Status Codes Reference
| Code | Meaning | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Authentication required |
| 403 | Forbidden | Insufficient permissions |
| 404 | Not Found | Resource doesn’t exist |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error |
Performance Optimization
1. Database Indexing
// Add indexes to frequently queried fields
userSchema.index({ email: 1 });
userSchema.index({ role: 1, isActive: 1 });
2. Response Compression
npm install compression
const compression = require('compression');
app.use(compression());
3. Caching
npm install node-cache
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
exports.getUsers = async (req, res, next) => {
const cacheKey = `users_${JSON.stringify(req.query)}`;
const cached = cache.get(cacheKey);
if (cached) {
return res.status(200).json(cached);
}
// ... fetch from database
cache.set(cacheKey, result);
res.status(200).json(result);
};
Deployment
Environment Configuration
## Production .env
NODE_ENV=production
PORT=8080
MONGODB_URI=mongodb+srv://user:[email protected]/myapi
JWT_SECRET=production-secret-key-very-long-and-secure
Process Manager (PM2)
npm install -g pm2
pm2 start server.js --name "my-api"
pm2 startup
pm2 save
Related Articles
- Cloudflare Workers: Serverless Web Application
- Setting Up Automated Backups with rsync, borgbackup and
- Advanced systemd Service Management and Unit File Creation
- Django Project Setup: Core Concepts
Conclusion
You’ve built a production-ready REST API with Node.js and Express, complete with database integration, validation, error handling, and security features. This foundation can scale from small projects to enterprise applications.
Key takeaways:
- Follow REST principles for consistent API design
- Validate input to prevent security vulnerabilities
- Handle errors gracefully with proper status codes
- Use middleware to keep code DRY and maintainable
- Document your API for developers (including future you)
Start with this solid foundation and extend it with features like authentication, file uploads, real-time capabilities with WebSockets, or microservices architecture as your needs grow.
References
[1] Fielding, R. T. (2000). Architectural Styles and the Design of Network-based Software Architectures. Doctoral dissertation, University of California, Irvine. Available at: https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm (Accessed: November 2025)
[2] Express.js Documentation. (2024). Express.js Guide. Available at: https://expressjs.com/en/guide/routing.html (Accessed: November 2025)
[3] Mozilla Developer Network. (2024). HTTP Status Codes. Available at: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status (Accessed: November 2025)