Key Takeaways
REST APIs have become the backbone of modern web applications, enabling seamless communication between clients and servers. Node.js, with its event-driven architecture and non-blocking I/O model, provides an excellent platform for building fast and scalable REST APIs. In this guide, we’ll walk through the complete process of creating a secure REST API using Node.js, Express, and MongoDB, complete with practical examples you can implement right away.
Set up the node.js project
Before diving into code, we need to set up our project environment. A well-structured project makes development and maintenance much easier in the long run.
Install required packages
First, create a new directory for your project and initialize it:
mkdir node-rest-api
cd node-rest-api
npm init -y
Next, install the core packages needed for our REST API:
npm install express mongoose dotenv
npm install nodemon cors helmet express-rate-limit --save-dev
These packages serve specific purposes:
- express: Web framework for creating server and routes
- mongoose: MongoDB object modeling tool
- dotenv: Loads environment variables from .env files
- cors: Enables Cross-Origin Resource Sharing
- helmet: Secures Express apps by setting various HTTP headers
- express-rate-limit: Basic rate-limiting middleware
- nodemon: Automatically restarts the server during development
Set up entry point file
Create a file named server.js in your project root. This will be our application’s entry point:
// server.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware setup
app.use(express.json());
app.use(cors());
app.use(helmet());
// Apply rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Root route
app.get('/', (req, res) => {
res.send('Welcome to our REST API in Node.js');
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Define folder structure
Organize your project with a clear folder structure to make it scalable and maintainable:
node-rest-api/
├── config/ # Configuration files
├── controllers/ # Route controllers
├── middleware/ # Custom middleware
├── models/ # Mongoose models
├── routes/ # API routes
├── utils/ # Utility functions
├── .env # Environment variables
├── .gitignore # Git ignore file
├── package.json # Project dependencies
└── server.js # Entry point
Create these folders to prepare for the next steps:
mkdir config controllers middleware models routes utils
Create the REST API in Node.js
Now that our project structure is ready, let’s build the actual REST API functionality.
Set up Express server
Our server.js file already contains the basic Express setup. Let’s enhance it by creating a configuration file for database connections.
Create config/db.js:
Staff Augmentation Service
Tap Into a Talent Ecosystem Powered by 1500+ Agencies and 1,900+ Projects. EMB’s Staff Augmentation Services Help You Stay Agile, Competitive, and Fully Resourced.
// config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
};
module.exports = connectDB;
Now, update server.js to use this connection:
// Add at the top of server.js after require statements
const connectDB = require('./config/db');
// Connect to database
connectDB();
// Rest of the code remains the same
Define CRUD API routes
Let’s create routes for a simple resource, such as products. First, create the model:
// models/Product.js
const mongoose = require('mongoose');
const ProductSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please add a name'],
trim: true,
maxlength: [50, 'Name cannot be more than 50 characters']
},
description: {
type: String,
required: [true, 'Please add a description'],
maxlength: [500, 'Description cannot be more than 500 characters']
},
price: {
type: Number,
required: [true, 'Please add a price']
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Product', ProductSchema);
Next, create a controller to handle product operations:
// controllers/productController.js
const Product = require('../models/Product');
// Get all products
exports.getProducts = async (req, res) => {
try {
const products = await Product.find();
res.status(200).json({ success: true, data: products });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
};
// Get single product
exports.getProduct = async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
res.status(200).json({ success: true, data: product });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
};
// Create new product
exports.createProduct = async (req, res) => {
try {
const product = await Product.create(req.body);
res.status(201).json({ success: true, data: product });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
};
// Update product
exports.updateProduct = async (req, res) => {
try {
const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
if (!product) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
res.status(200).json({ success: true, data: product });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
};
// Delete product
exports.deleteProduct = async (req, res) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);
if (!product) {
return res.status(404).json({ success: false, error: 'Product not found' });
}
res.status(200).json({ success: true, data: {} });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
};
Use middleware for routing
Now, let’s create the routes and use Express Router as middleware:
// routes/productRoutes.js
const express = require('express');
const router = express.Router();
const {
getProducts,
getProduct,
createProduct,
updateProduct,
deleteProduct
} = require('../controllers/productController');
router.route('/')
.get(getProducts)
.post(createProduct);
router.route('/:id')
.get(getProduct)
.put(updateProduct)
.delete(deleteProduct);
module.exports = router;
Finally, update server.js to use these routes:
// Add after middleware setup in server.js
const productRoutes = require('./routes/productRoutes');
// Mount routes
app.use('/api/products', productRoutes);
Add database support with MongoDB
We’ve already set up our database connection and created a model. Now let’s look at more advanced database operations.
Connect MongoDB using Mongoose
For a production application, we need to handle database connection errors and implement retry logic. Let’s update our config/db.js file:
// config/db.js (enhanced version)
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
serverSelectionTimeoutMS: 5000
});
console.log(`MongoDB Connected: ${conn.connection.host}`);
mongoose.connection.on('error', err => {
console.error(`MongoDB connection error: ${err}`);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB disconnected, trying to reconnect...');
setTimeout(connectDB, 5000);
});
} catch (err) {
console.error(`Error: ${err.message}`);
// Attempt to reconnect after 5 seconds
console.log('Attempting to reconnect to MongoDB...');
setTimeout(connectDB, 5000);
}
};
module.exports = connectDB;
Create a .env file in the root directory to store your MongoDB connection string:
PORT=3000
MONGO_URI=mongodb://localhost:27017/node-rest-api
NODE_ENV=development
Create and use data models
Let’s enhance our product model with more advanced features:
// models/Product.js (enhanced)
const mongoose = require('mongoose');
const slugify = require('slugify');
const ProductSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please add a name'],
unique: true,
trim: true,
maxlength: [50, 'Name cannot be more than 50 characters']
},
slug: String,
description: {
type: String,
required: [true, 'Please add a description'],
maxlength: [500, 'Description cannot be more than 500 characters']
},
price: {
type: Number,
required: [true, 'Please add a price']
},
category: {
type: String,
required: [true, 'Please add a category'],
enum: ['Electronics', 'Books', 'Clothing', 'Food']
},
stock: {
type: Number,
required: [true, 'Please add stock quantity'],
min: [0, 'Stock cannot be negative']
},
createdAt: {
type: Date,
default: Date.now
}
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Create product slug from name
ProductSchema.pre('save', function(next) {
this.slug = slugify(this.name, { lower: true });
next();
});
// Virtual for formatted price
ProductSchema.virtual('formattedPrice').get(function() {
return `$${this.price.toFixed(2)}`;
});
module.exports = mongoose.model('Product', ProductSchema);
Remember to install the slugify package:
npm install slugify
Handle data from API endpoints
Let’s improve our controller to handle filtering, sorting, and pagination:
// controllers/productController.js (enhanced getProducts method)
exports.getProducts = async (req, res) => {
try {
// Copy req.query
const reqQuery = { ...req.query };
// Fields to exclude
const removeFields = ['select', 'sort', 'page', 'limit'];
// Loop over removeFields and delete them from reqQuery
removeFields.forEach(param => delete reqQuery[param]);
// Create query string
let queryStr = JSON.stringify(reqQuery);
// Create operators ($gt, $gte, etc)
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`);
// Finding resource
let query = Product.find(JSON.parse(queryStr));
// Select Fields
if (req.query.select) {
const fields = req.query.select.split(',').join(' ');
query = query.select(fields);
}
// Sort
if (req.query.sort) {
const sortBy = req.query.sort.split(',').join(' ');
query = query.sort(sortBy);
} else {
query = query.sort('-createdAt');
}
// Pagination
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 25;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
const total = await Product.countDocuments();
query = query.skip(startIndex).limit(limit);
// Executing query
const products = await query;
// Pagination result
const pagination = {};
if (endIndex < total) {
pagination.next = {
page: page + 1,
limit
};
}
if (startIndex > 0) {
pagination.prev = {
page: page - 1,
limit
};
}
res.status(200).json({
success: true,
count: products.length,
pagination,
data: products
});
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
};
Wrap up your secure Rest API in Node.js project
We’ve built a robust REST API in Node.js with Express and MongoDB. Our API includes proper project structure, CRUD operations, data validation, error handling, and advanced features like filtering, sorting, and pagination. This foundation can be extended for various applications requiring secure server-client communication.
To further enhance your API, consider adding:
- JWT authentication
- Role-based access control
- API documentation with Swagger
- Comprehensive testing
- Logging and monitoring
With the skills learned here, you can create powerful REST APIs to support web and mobile applications, microservices, or any system requiring HTTP-based communication.
FAQ
Q1. How do I create a secure REST API in Node.js?
Creating a secure REST API in Node.js involves multiple layers of protection. Start by using HTTPS, implementing proper authentication (JWT, OAuth), validating all inputs, using security packages like Helmet, implementing rate limiting, sanitizing database queries, encrypting sensitive data, and keeping dependencies updated. Adding CORS protection and security headers also significantly improves your API’s security posture.
Q2. What is a real-world REST API example in Node.js?
A common real-world example is an e-commerce API that handles products, users, and orders. It would include endpoints for user registration/authentication, product browsing with filtering and searching, shopping cart management, checkout processing, and order history. Such an API would connect to payment gateways, handle inventory updates, and possibly integrate with shipping services, while maintaining proper authentication and authorization throughout.
Q3. How can I test my Node.js REST API easily?
Testing a REST API can be done at multiple levels. For quick manual testing, use tools like Postman or Insomnia. For automated testing, use frameworks like Mocha, Chai, and Supertest for unit and integration tests. Jest is also popular. Write tests for each endpoint, checking successful operations and error handling. Mock external services and databases using tools like Sinon.js. Finally, set up a CI/CD pipeline to run tests automatically before deployment.
Q4. What is the best way to organize REST APIs in Node?
The best organization follows the MVC pattern with some modifications. Structure your project with clear separation of concerns: routes (defining endpoints), controllers (handling request/response), services (business logic), and models (data structure). Group related endpoints, use middleware for cross-cutting concerns, implement consistent error handling, and document your API thoroughly. The folder structure should reflect this organization, making it intuitive for developers to navigate and maintain.
