Building a REST API with Node.js and Express
Learn how to build a production-ready RESTful API using Node.js, Express, and MongoDB with authentication, validation, and best practices.
Building a REST API with Node.js and Express
Creating a robust REST API is a fundamental skill for backend developers. In this guide, we'll build a complete RESTful API using Node.js and Express, covering authentication, data validation, error handling, and best practices.
What We'll Build
We'll create a blog API with the following features:
- User authentication (JWT)
- CRUD operations for posts
- Input validation
- Error handling
- Security best practices
- API documentation
Prerequisites
- Node.js 18+ installed
- Basic JavaScript knowledge
- Understanding of REST principles
- MongoDB installed (or MongoDB Atlas account)
Project Setup
Create a new project:
mkdir blog-api
cd blog-api
npm init -y
Install dependencies:
npm install express mongoose bcryptjs jsonwebtoken dotenv express-validator
npm install -D nodemon
Update package.json:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
Project Structure
blog-api/
├── config/
│ └── db.js
├── controllers/
│ ├── authController.js
│ └── postController.js
├── middleware/
│ ├── auth.js
│ └── errorHandler.js
├── models/
│ ├── User.js
│ └── Post.js
├── routes/
│ ├── auth.js
│ └── posts.js
├── .env
└── server.js
Environment Configuration
Create .env:
PORT=3000
MONGODB_URI=mongodb://localhost:27017/blog-api
JWT_SECRET=your-secret-key-change-this
NODE_ENV=development
Database Connection
config/db.js:
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log('MongoDB connected');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
module.exports = connectDB;
Models
User Model
models/User.js:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
password: {
type: String,
required: true,
minlength: 6
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
Post Model
models/Post.js:
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
maxlength: 200
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: [{
type: String,
trim: true
}],
published: {
type: Boolean,
default: false
},
views: {
type: Number,
default: 0
}
}, {
timestamps: true
});
// Index for better query performance
postSchema.index({ title: 'text', content: 'text' });
module.exports = mongoose.model('Post', postSchema);
Authentication Middleware
middleware/auth.js:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
// Get token from header
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Find user
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
throw new Error();
}
// Attach user to request
req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid authentication token' });
}
};
// Admin middleware
const admin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
};
module.exports = { auth, admin };
Error Handler Middleware
middleware/errorHandler.js:
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
// Mongoose validation error
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ errors });
}
// Mongoose duplicate key error
if (err.code === 11000) {
return res.status(400).json({
error: 'Duplicate field value entered'
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
// Default error
res.status(err.statusCode || 500).json({
error: err.message || 'Server Error'
});
};
module.exports = errorHandler;
Controllers
Auth Controller
controllers/authController.js:
const jwt = require('jsonwebtoken');
const { validationResult } = require('express-validator');
const User = require('../models/User');
// Generate JWT token
const generateToken = (userId) => {
return jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: '7d'
});
};
// Register user
exports.register = async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, email, password } = req.body;
// Create user
const user = await User.create({ username, email, password });
// Generate token
const token = generateToken(user._id);
res.status(201).json({
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
},
token
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// Login user
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token
const token = generateToken(user._id);
res.json({
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
},
token
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// Get current user
exports.me = async (req, res) => {
res.json({ user: req.user });
};
Routes
Auth Routes
routes/auth.js:
const express = require('express');
const { body } = require('express-validator');
const authController = require('../controllers/authController');
const { auth } = require('../middleware/auth');
const router = express.Router();
// Validation rules
const registerValidation = [
body('username').trim().isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
body('email').isEmail().withMessage('Invalid email address'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
];
const loginValidation = [
body('email').isEmail().withMessage('Invalid email address'),
body('password').notEmpty().withMessage('Password is required')
];
// Routes
router.post('/register', registerValidation, authController.register);
router.post('/login', loginValidation, authController.login);
router.get('/me', auth, authController.me);
module.exports = router;
Main Server File
server.js:
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/db');
const errorHandler = require('./middleware/errorHandler');
const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/posts');
const app = express();
// Connect to database
connectDB();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/posts', postRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Error handler
app.use(errorHandler);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Testing the API
Register a User
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"email": "john@example.com",
"password": "password123"
}'
Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "password123"
}'
Get Current User
curl -X GET http://localhost:3000/api/auth/me \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Security Best Practices
- Environment Variables: Never commit
.envfiles - Password Hashing: Always hash passwords before storing
- Input Validation: Validate all user inputs
- Rate Limiting: Implement rate limiting to prevent abuse
- CORS: Configure CORS properly for production
- HTTPS: Always use HTTPS in production
- Error Messages: Don't expose sensitive information in errors
Next Steps
- Add rate limiting with
express-rate-limit - Implement pagination for list endpoints
- Add comprehensive logging
- Set up API documentation with Swagger
- Add unit and integration tests
- Implement caching with Redis
- Set up CI/CD pipeline
Conclusion
You now have a solid foundation for building REST APIs with Node.js and Express. This setup includes authentication, validation, error handling, and follows industry best practices.
Remember to always validate inputs, handle errors properly, and keep security in mind when building APIs!
Related Guides
Getting Started with Next.js 14: A Complete Guide
Learn how to build modern web applications with Next.js 14, including App Router, Server Components, and best practices for performance and SEO.
Mastering TypeScript: From Basics to Advanced
A comprehensive guide to TypeScript covering type annotations, interfaces, generics, and advanced patterns for building type-safe applications.