RESTful APIs built with Node.js and Express remain a popular choice for backend development due to their simplicity, scalability, and performance. Express provides a minimal framework for building web applications while allowing developers to add middleware and routing as needed.
Key characteristics of RESTful APIs:
Initialize a new Node.js project:
mkdir express-api
cd express-api
npm init -y
npm install express body-parser cors
npm install --save-dev nodemon
Basic Express server setup (server.js
):
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(bodyParser.json());
// Basic route
app.get('/', (req, res) => {
res.json({ message: 'API is running' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Express allows clean routing structure for RESTful resources. For a products
resource:
// routes/products.js
const express = require('express');
const router = express.Router();
// Controller would be imported here
const productsController = require('../controllers/products');
router.get('/', productsController.getAllProducts);
router.post('/', productsController.createProduct);
router.get('/:id', productsController.getProductById);
router.put('/:id', productsController.updateProduct);
router.delete('/:id', productsController.deleteProduct);
module.exports = router;
// controllers/products.js
let products = [
{ id: 1, name: 'Laptop', price: 999.99 },
{ id: 2, name: 'Phone', price: 699.99 }
];
exports.getAllProducts = (req, res) => {
res.json(products);
};
exports.getProductById = (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));
if (!product) return res.status(404).json({ message: 'Product not found' });
res.json(product);
};
exports.createProduct = (req, res) => {
const { name, price } = req.body;
if (!name || !price) {
return res.status(400).json({ message: 'Name and price are required' });
}
const newProduct = {
id: products.length + 1,
name,
price: parseFloat(price)
};
products.push(newProduct);
res.status(201).json(newProduct);
};
Create validation middleware:
// middleware/validateProduct.js
exports.validateProduct = (req, res, next) => {
const { name, price } = req.body;
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Invalid product name' });
}
if (!price || isNaN(parseFloat(price))) {
return res.status(400).json({ error: 'Invalid price' });
}
next();
};
Centralized error handling middleware:
// middleware/errorHandler.js
exports.errorHandler = (err, req, res, next) => {
console.error(err.stack);
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
return res.status(400).json({ error: 'Invalid JSON payload' });
}
res.status(500).json({ error: 'Something went wrong' });
};
Register middleware in main app:
// server.js
const { errorHandler } = require('./middleware/errorHandler');
const { validateProduct } = require('./middleware/validateProduct');
// Apply to specific routes
app.post('/products', validateProduct, productsController.createProduct);
// Global error handler (must be last middleware)
app.use(errorHandler);
Install MongoDB dependencies:
npm install mongoose
Database connection setup:
// config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/products_api', {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('MongoDB connected');
} catch (err) {
console.error('Database connection error:', err);
process.exit(1);
}
};
module.exports = connectDB;
// models/Product.js
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Product name is required'],
trim: true,
maxlength: [100, 'Name cannot exceed 100 characters']
},
price: {
type: Number,
required: [true, 'Price is required'],
min: [0, 'Price must be positive']
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Product', productSchema);
// controllers/products.js
const Product = require('../models/Product');
exports.getAllProducts = async (req, res, next) => {
try {
const products = await Product.find();
res.json(products);
} catch (err) {
next(err);
}
};
exports.createProduct = async (req, res, next) => {
try {
const product = new Product(req.body);
await product.save();
res.status(201).json(product);
} catch (err) {
next(err);
}
};
Install Swagger dependencies:
npm install swagger-ui-express swagger-jsdoc
Swagger setup:
// config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Products API',
version: '1.0.0',
description: 'A simple products API'
},
servers: [
{
url: 'http://localhost:3000/api/v1'
}
]
},
apis: ['./routes/*.js']
};
const specs = swaggerJsdoc(options);
module.exports = (app) => {
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
};
/**
* @swagger
* /products:
* get:
* summary: Returns all products
* responses:
* 200:
* description: A list of products
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Product'
*/
router.get('/', productsController.getAllProducts);
Install dependencies:
npm install jsonwebtoken bcryptjs
User model:
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6,
select: false
}
});
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);
Auth middleware:
// middleware/auth.js
const jwt = require('jsonwebtoken');
exports.protect = async (req, res, next) => {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({ error: 'Not authorized' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
Implement versioning in routes:
// server.js
const v1Router = require('./routes/v1');
app.use('/api/v1', v1Router);
Folder structure:
routes/
v1/
products.js
users.js
v2/
products.js
Install testing dependencies:
npm install --save-dev jest supertest mongodb-memory-server
Test setup:
// tests/products.test.js
const request = require('supertest');
const app = require('../server');
const Product = require('../models/Product');
describe('Products API', () => {
beforeEach(async () => {
await Product.deleteMany();
});
it('should create a new product', async () => {
const res = await request(app)
.post('/api/v1/products')
.send({
name: 'Test Product',
price: 19.99
});
expect(res.statusCode).toEqual(201);
expect(res.body).toHaveProperty('_id');
expect(res.body.name).toBe('Test Product');
});
});
Use dotenv
for environment variables:
npm install dotenv
.env
file:
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/products_api
JWT_SECRET=your-secret-key
Security:
Performance:
Monitoring:
The flexibility of Express combined with Node.js's performance makes this stack ideal for building scalable APIs that can evolve with your application's requirements.