Backend
Building RESTful APIs with Node.js and Express

December 26, 2025
22 min read
Node.jsExpressAPIBackendJavaScriptREST
Building RESTful APIs with Node.js and Express
Node.js and Express provide a powerful combination for building fast, scalable REST APIs. This comprehensive guide covers everything from setup to production deployment.
Why Node.js for APIs?
- Non-blocking I/O: Handles concurrent requests efficiently
- JavaScript everywhere: Same language for frontend and backend
- NPM ecosystem: Vast library of packages
- Fast execution: V8 engine performance
- Easy to learn: Familiar syntax for web developers
Project Setup
# Create project directory mkdir node-api && cd node-api # Initialize npm project npm init -y # Install dependencies npm install express mongoose dotenv cors helmet morgan npm install -D nodemon typescript @types/node @types/express
Project Structure
node-api/
├── src/
│ ├── controllers/
│ │ └── userController.js
│ ├── models/
│ │ └── User.js
│ ├── routes/
│ │ └── userRoutes.js
│ ├── middleware/
│ │ ├── auth.js
│ │ └── errorHandler.js
│ ├── config/
│ │ └── database.js
│ └── app.js
├── .env
├── .gitignore
└── package.json
Basic Express Server
// src/app.js const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); const dotenv = require('dotenv'); dotenv.config(); const app = express(); // Middleware app.use(helmet()); // Security headers app.use(cors()); // CORS app.use(morgan('dev')); // Logging app.use(express.json()); // Parse JSON bodies app.use(express.urlencoded({ extended: true })); // Routes app.get('/', (req, res) => { res.json({ message: 'Welcome to the API' }); }); // Health check app.get('/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString() }); }); // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!' }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Route not found' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(\`Server running on port \${PORT}\`); }); module.exports = app;
RESTful Routes
// src/routes/userRoutes.js const express = require('express'); const router = express.Router(); const userController = require('../controllers/userController'); const auth = require('../middleware/auth'); // Public routes router.post('/register', userController.register); router.post('/login', userController.login); // Protected routes router.get('/', auth, userController.getAllUsers); router.get('/:id', auth, userController.getUserById); router.put('/:id', auth, userController.updateUser); router.delete('/:id', auth, userController.deleteUser); module.exports = router;
User Model with Mongoose
// src/models/User.js const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); 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: [/^\\S+@\\S+\\.\\S+$/, 'Please enter a valid email'] }, password: { type: String, required: [true, 'Password is required'], minlength: [6, 'Password must be at least 6 characters'], select: false }, role: { type: String, enum: ['user', 'admin'], default: 'user' }, createdAt: { type: Date, default: Date.now } }); // Hash password before saving userSchema.pre('save', async function(next) { if (!this.isModified('password')) return next(); this.password = await bcrypt.hash(this.password, 12); next(); }); // Compare password method userSchema.methods.comparePassword = async function(candidatePassword) { return await bcrypt.compare(candidatePassword, this.password); }; module.exports = mongoose.model('User', userSchema);
User Controller
// src/controllers/userController.js const User = require('../models/User'); const jwt = require('jsonwebtoken'); // Generate JWT token const generateToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }); }; // @desc Register user // @route POST /api/users/register exports.register = async (req, res) => { try { const { name, email, password } = req.body; // Check if user exists const existingUser = await User.findOne({ email }); if (existingUser) { return res.status(400).json({ error: 'Email already registered' }); } // Create user const user = await User.create({ name, email, password }); // Generate token const token = generateToken(user._id); res.status(201).json({ success: true, token, user: { id: user._id, name: user.name, email: user.email, role: user.role } }); } catch (error) { res.status(500).json({ error: error.message }); } }; // @desc Login user // @route POST /api/users/login exports.login = async (req, res) => { try { const { email, password } = req.body; // Validate input if (!email || !password) { return res.status(400).json({ error: 'Please provide email and password' }); } // Find user and include password const user = await User.findOne({ email }).select('+password'); if (!user || !(await user.comparePassword(password))) { return res.status(401).json({ error: 'Invalid credentials' }); } // Generate token const token = generateToken(user._id); res.json({ success: true, token, user: { id: user._id, name: user.name, email: user.email, role: user.role } }); } catch (error) { res.status(500).json({ error: error.message }); } }; // @desc Get all users // @route GET /api/users exports.getAllUsers = async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 10; const skip = (page - 1) * limit; const users = await User.find() .skip(skip) .limit(limit) .sort({ createdAt: -1 }); const total = await User.countDocuments(); res.json({ success: true, count: users.length, total, page, pages: Math.ceil(total / limit), data: users }); } catch (error) { res.status(500).json({ error: error.message }); } }; // @desc Get single user // @route GET /api/users/:id exports.getUserById = async (req, res) => { try { const user = await User.findById(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ success: true, data: user }); } catch (error) { res.status(500).json({ error: error.message }); } }; // @desc Update user // @route PUT /api/users/:id exports.updateUser = async (req, res) => { try { const { name, email } = req.body; const user = await User.findByIdAndUpdate( req.params.id, { name, email }, { new: true, runValidators: true } ); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ success: true, data: user }); } catch (error) { res.status(500).json({ error: error.message }); } }; // @desc Delete user // @route DELETE /api/users/:id exports.deleteUser = async (req, res) => { try { const user = await User.findByIdAndDelete(req.params.id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ success: true, message: 'User deleted' }); } catch (error) { res.status(500).json({ error: error.message }); } };
Authentication Middleware
// src/middleware/auth.js const jwt = require('jsonwebtoken'); const User = require('../models/User'); module.exports = async (req, res, next) => { try { // Get token from header const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Access denied. No token provided.' }); } const token = authHeader.split(' ')[1]; // Verify token const decoded = jwt.verify(token, process.env.JWT_SECRET); // Check if user still exists const user = await User.findById(decoded.id); if (!user) { return res.status(401).json({ error: 'User no longer exists' }); } // Attach user to request req.user = user; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } };
Input Validation
// src/middleware/validate.js const { body, validationResult } = require('express-validator'); exports.validateUser = [ body('name') .trim() .notEmpty().withMessage('Name is required') .isLength({ max: 50 }).withMessage('Name cannot exceed 50 characters'), body('email') .trim() .notEmpty().withMessage('Email is required') .isEmail().withMessage('Please enter a valid email') .normalizeEmail(), body('password') .notEmpty().withMessage('Password is required') .isLength({ min: 6 }).withMessage('Password must be at least 6 characters'), (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } next(); } ];
Rate Limiting
// src/middleware/rateLimiter.js const rateLimit = require('express-rate-limit'); exports.apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests, please try again later' } }); exports.authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 failed attempts per hour message: { error: 'Too many login attempts, please try again after an hour' } });
Environment Configuration
# .env NODE_ENV=development PORT=3000 MONGODB_URI=mongodb://localhost:27017/myapi JWT_SECRET=your-super-secret-jwt-key-change-in-production JWT_EXPIRES_IN=7d
Conclusion
This guide covered building a production-ready REST API with Node.js and Express. Key takeaways:
- Structure your project for scalability
- Implement proper authentication and authorization
- Validate all inputs
- Handle errors gracefully
- Use rate limiting for security
- Document your API
- Test thoroughly before deployment
With these foundations, you can build robust APIs for any application!

About Dimuthu Wayaman
Mobile Application Developer and UI Designer specializing in Flutter development. Passionate about creating beautiful, functional mobile applications and sharing knowledge with the developer community.