REST APIs remain the backbone of modern web and mobile applications. This tutorial walks you through building a production-ready API from scratch using Node.js and Express, incorporating 2026 best practices.
Prerequisites
-
Node.js 20+ installed
-
Basic JavaScript knowledge
-
A code editor (VS Code recommended)
-
Postman or similar for testing
Step 1: Project Setup Create a new directory and initialize your project:
bash
mkdir node-api-tutorial
cd node-api-tutorial
npm init -y
Install dependencies:
bash
npm install express mongoose dotenv bcryptjs jsonwebtoken express-validator cors helmet morgan
npm install --save-dev nodemon jest supertest
Step 2: Basic Server Structure Create
server.js:JavaScript
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
require('dotenv').config();
const app = express();
// Middleware
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
// Routes
app.use('/api/users', require('./routes/users'));
app.use('/api/posts', require('./routes/posts'));
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Step 3: Database Connection Create
config/db.js:JavaScript
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI);
console.log('MongoDB Connected');
} catch (error) {
console.error('Database connection failed:', error);
process.exit(1);
}
};
module.exports = connectDB;
Step 4: User Model Create
models/User.js:JavaScript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true, minlength: 6 },
createdAt: { type: Date, default: Date.now }
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
module.exports = mongoose.model('User', userSchema);
Step 5: Authentication Middleware Create
middleware/auth.js:JavaScript
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token, authorization denied' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded.userId;
next();
} catch (error) {
res.status(401).json({ error: 'Token is invalid' });
}
};
Step 6: Validation Create
middleware/validation.js:JavaScript
const { body, validationResult } = require('express-validator');
const userValidation = [
body('name').trim().isLength({ min: 2 }).withMessage('Name must be at least 2 characters'),
body('email').isEmail().normalizeEmail().withMessage('Valid email required'),
body('password').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();
}
];
module.exports = { userValidation };
Step 7: User Routes Create
routes/users.js:JavaScript
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = require('../middleware/auth');
const { userValidation } = require('../middleware/validation');
// Register
router.post('/register', userValidation, async (req, res) => {
try {
const { name, email, password } = req.body;
let user = await User.findOne({ email });
if (user) return res.status(400).json({ error: 'User already exists' });
user = new User({ name, email, password });
await user.save();
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.status(201).json({ token, user: { id: user._id, name, email } });
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(400).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
res.json({ token, user: { id: user._id, name: user.name, email } });
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
// Get Profile
router.get('/me', auth, async (req, res) => {
try {
const user = await User.findById(req.user).select('-password');
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;
Step 8: Environment Variables Create
.env:plain
PORT=3000
MONGODB_URI=mongodb://localhost:27017/node-api
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
Step 9: Testing Create
tests/users.test.js:JavaScript
const request = require('supertest');
const app = require('../server');
describe('User API', () => {
it('should register a new user', async () => {
const res = await request(app)
.post('/api/users/register')
.send({
name: 'Test User',
email: 'test@example.com',
password: 'password123'
});
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('token');
});
});
Step 10: Deployment For production:
-
Use
pm2for process management -
Enable HTTPS with Let's Encrypt
-
Set up environment variables securely
-
Use a reverse proxy (Nginx)
-
Implement rate limiting with
express-rate-limit
Conclusion You've built a secure, validated REST API with authentication. This foundation scales to production applications. Add database indexing, caching with Redis, and API documentation with Swagger for complete production readiness.