Creating RESTful APIs is a fundamental skill for modern web developers. In this updated tutorial, you'll learn how to build a clean, maintainable REST API using the latest versions of **Node.js**, **Express.js**, **MongoDB**, and **Mongoose** — with best practices for structure, error handling, and deployment.
By the end of this guide, you'll have a production-ready API that supports full CRUD operations, is ready for frontend integration, and can be deployed to the cloud.
🔥 What's New in This 2025 Edition?
- Updated for **Node.js 20+, Express 5**, and **Mongoose 8**
- Clean MVC-style project structure with **controllers and routes**
- Built-in **error handling**, **input validation**, and environment variables
- Modern **deployment guide using Render**
- Tested with **cURL** and structured for frontend consumption (CORS-ready)
📦 Technologies Used
- **Node.js** – JavaScript runtime
- **Express.js** – Minimal and flexible web framework
- **MongoDB** – NoSQL document database
- **Mongoose** – ODM for MongoDB
- **dotenv** – Environment variable management
- **Render** – Free hosting platform for Node.js
Let’s get started with setting up the project!
Project Setup (Updated for 2025)
Let’s start by creating a clean Node.js project using modern best practices, including ES Modules and environment configuration.
1. Create the Project Directory
mkdir node-express-mongodb-restapi
cd node-express-mongodb-restapi
2. Initialize a Node.js Project
npm init -y
Then edit the package.json file and add this line to use ES Modules:
"type": "module"
✅ Using "type": "module" enables ES6 import/export syntax, which is now standard in modern Node.js.
3. Install Dependencies
We'll install the necessary packages for building a RESTful API:
npm install express mongoose dotenv
npm install --save-dev nodemon
- express – Web framework
- mongoose – MongoDB ODM
- dotenv – Manage environment variables
- nodemon – Auto-restarts server during development
4. Set up Project Structure
Here’s the recommended folder structure:
node-express-mongodb-restapi/
├── controllers/
│ └── productController.js
├── models/
│ └── Product.js
├── routes/
│ └── productRoutes.js
├── config/
│ └── db.js
├── .env
├── server.js
└── package.json
You’ll create these files as we go.
💡 Keeping the app modular (routes, controllers, models) makes it scalable and maintainable.
5. Configure .env
Create a .env file at the root of your project to store config values like your MongoDB connection URI:
PORT=3000
MONGO_URI=mongodb://localhost:27017/product
You can later replace MONGO_URI with your MongoDB Atlas URI for production deployments.
Connecting to MongoDB (Updated for 2025)
To connect our Node.js application to MongoDB, we will use **Mongoose**, an ODM (Object Data Modeling) library that provides a straightforward way to interact with MongoDB.
1. Install MongoDB Dependencies
We already installed `mongoose` as part of our project setup, but to recap, here’s how you can install it:
npm install mongoose
2. Create a Database Configuration File
To keep things organized, create a config/ directory with a file called db.js. This will handle the MongoDB connection.
mkdir config
touch config/db.js
Inside config/db.js, add the following code to connect to MongoDB:
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config(); // Load environment variables from .env
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected');
} catch (err) {
console.error(err.message);
process.exit(1); // Exit process with failure
}
};
export default connectDB;
This code:
- Loads the MongoDB URI from the .env file
- Uses mongoose.connect() to establish a connection to the MongoDB database
- Log success or failure to the console
💡 MongoDB Atlas (cloud) can be used by simply updating MONGO_URI in the .env file with the connection string.
3. Use the Connection in server.js
Next, modify your server.js to import and use this connection logic:i
mport express from "express";
import dotenv from "dotenv";
import connectDB from "./config/db.js";
import productRoutes from "./routes/productRoutes.js";
dotenv.config();
connectDB();
const app = express();
app.use(express.json());
app.use("/api", productRoutes);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
4. Test the MongoDB Connection
Now, you should be able to start your server:
npm run dev
You should see the message "MongoDB connected" in your console. This confirms that the connection to your MongoDB database is successfully established.
5. MongoDB Atlas (Optional)
If you’re using MongoDB Atlas for cloud hosting, replace the MONGO_URI in .env with the connection string provided by Atlas.
- Go to MongoDB Atlas.
- Create a cluster (or use an existing one).
- In the "Connect" tab, choose "Connect your application" and copy the provided connection string.
MONGO_URI=mongodb+srv://yourusername:[email protected]/tutorialdb?retryWrites=true&w=majority
Make sure to replace yourusername and yourpassword with your actual MongoDB credentials.
Creating the Mongoose Model
Mongoose allows us to define **schemas** and **models**, which provide a structured way to interact with data in MongoDB. In this section, we’ll create a model for our `Product` resource.
1. Define the Tutorial Schema
In your `models/` directory, create a file called `Product.js` to define the schema and model for our tutorials.
mkdir models
touch models/Product.js
Inside models/Product.js, define the schema and create the model as follows:
import mongoose from "mongoose";
const productSchema = new mongoose.Schema(
{
prod_name: {
type: String,
required: [true, "Product name is required"],
minlength: [3, "Product name should be at least 10 characters"]
},
prod_desc: {
type: String,
required: [true, "Product description is required"],
minlength: [10, "Product description should be at least 10 characters"]
},
prod_price: {
type: Number,
required: [true, "Product price is required"],
min: [1, "Zero price not allowed"],
max: [1000, "Price too high"]
},
updated_at: { type: Date, default: Date.now }
},
{ timestamps: true }
);
const Product = mongoose.model("Product", productSchema);
export default Product;
2. Explanation of the Schema
- title: The name of the tutorial is, required field.
- description: A brief description of the tutorial (optional).
- published: A boolean flag to mark the tutorial as published (defaults to false).
- Timestamps: This option automatically adds createdAt and updatedAt fields, useful for tracking when tutorials are created or modified.
3. Using the Model
Once the Product model is created, we can use it to interact with the database in our controllers. Here are some examples:
- Create a new Product: You can create a new product by instantiating the Product model and saving it to the database.
- Find products: Use Product() to retrieve all products.
- Update a product: Use Product.findByIdAndUpdate() to modify a specific product.
- Delete a product: Use Product.findByIdAndDelete() to remove a product by its ID.
4. Model Validation
Mongoose also supports validation. For example, you can add more rules to the schema if you want to enforce validation rules for fields like name or price.
To enforce the name to be a non-empty string:
prod_name: {
type: String,
required: [true, "Product name is required"],
minlength: [3, "Product name should be at least 10 characters"]
},
To enforce a price is a non-empty number and has allowed a minimum or maximum price:
prod_price: {
type: Number,
required: [true, "Product price is required"],
min: [1, "Zero price not allowed"],
max: [1000, "Price too high"]
},
5. Using the Model in Controllers
Now that we've defined the model, let's use it in our controller (in the next section, we already covered productController.js, which handles CRUD operations).
Creating the Controller (Updated for 2025)
Now that we’ve defined the Mongoose model, we’ll create a controller to handle the logic for each API endpoint: creating, reading, updating, and deleting products.
1. Create the Controller File
Create a `controllers` folder and a file named `productController.js`:
mkdir controllers
touch controllers/tutorialController.js
2. Add Controller Logic
Inside controllers/productController.js, add the following code:
import Product from "../models/Product.js";
// POST /api/products
export const createProduct = async (req, res) => {
try {
const product = new Product(req.body);
const saved = await product.save();
res.status(201).json(saved);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
// GET /api/products
export const getAllProducts = async (req, res) => {
try {
const products = await Product.find();
res.json(products);
} catch (err) {
res.status(500).json({ error: err.message });
}
};
// GET /api/products/:id
export const getProductById = async (req, res) => {
try {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({ message: "Product not found" });
}
res.json(product);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
// PUT /api/products/:id
export const updateProduct = async (req, res) => {
try {
const updated = await Product.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
if (!updated) {
return res.status(404).json({ message: "Product not found" });
}
res.json(updated);
} catch (err) {
res.status(400).json({ error: err.message });
}
};
// DELETE /api/products/:id
export const deleteProduct = async (req, res) => {
try {
const deleted = await Product.findByIdAndDelete(req.params.id);
if (!deleted) {
return res.status(404).json({ message: "Product not found" });
}
res.json({ message: "Product deleted" });
} catch (err) {
res.status(400).json({ error: err.message });
}
};
3. What Each Function Does
- createProduct – Creates and saves a new product to MongoDB.
- getAllProducts – Fetches all products.
- getProductById – Retrieves a specific product by its _id.
- updateProduct – Updates a product by _id.
- deleteProduct – Deletes a product by _id.
Each function handles errors gracefully and sends back proper HTTP status codes and messages.
4. Exporting Functions
We're using named exports (export const ...) so we can easily import these in the routes file:
import {
createProduct,
getAllProducts,
getProductById,
updateProduct,
deleteProduct
} from '../controllers/productController.js';
With the controller created, your API now has all the logic it needs to handle incoming requests and interact with the database.
Creating the Routes (Updated for 2025)
Now that we’ve set up our controller and model, let’s define the Express routes that map HTTP requests to our controller functions.
1. Create a Route File
Create a new folder named `routes` and add a file called `productRoutes.js`:
mkdir routes
touch routes/tutorialRoutes.js
2. Define the Routes
Add the following code to routes/productRoutes.js:
import express from "express";
import {
createProduct,
getAllProducts,
getProductById,
updateProduct,
deleteProduct
} from "../controllers/productController.js";
const router = express.Router();
router.post("/products", createProduct);
router.get("/products", getAllProducts);
router.get("/products/:id", getProductById);
router.put("/products/:id", updateProduct);
router.delete("/products/:id", deleteProduct);
export default router;
Each route corresponds to a standard REST operation:
- POST → Create a new tutorial
- GET → Retrieve one or all tutorials
- PUT → Update an existing tutorial
- DELETE → Remove a tutorial
3. Integrate Routes in server.js
Make sure your server.js includes the route file:
import productRoutes from './routes/productRoutes.js';
// API route prefix
app.use('/api', productRoutes);
Now, all your API endpoints will be prefixed with /api, for example:
- GET /api/products – Get all products
- POST /api/products – Create a product
- GET /api/products/:id – Get a single product
- PUT /api/products/:id – Update a product
- DELETE /api/products/:id – Delete a product
4. Test Your Routes
Use a REST client like Postman, Insomnia, or curl to test your API endpoints.
For example, to test creating a product with curl:
curl -X POST http://localhost:3000/api/products \
-H "Content-Type: application/json" \
-d '{"prod_name": "iPhone 17", "prod_desc": "The latest Apple product in 2026", "prod_price": 999}'
To test getting a list of products with curl:
curl -X GET http://localhost:3000/api/products
It should respond:
[{"_id":"6816c60f1812ad3e4da5669c","prod_name":"iPhone 17","prod_desc":"The latest Apple product in 2026","prod_price":999,"updated_at":"2025-05-04T01:42:39.750Z","createdAt":"2025-05-04T01:42:39.752Z","updatedAt":"2025-05-04T01:42:39.752Z","__v":0}]
To test getting a product by ID with curl:
curl -X GET http://localhost:3000/api/products/6816c60f1812ad3e4da5669c
It should respond:
{"_id":"6816c60f1812ad3e4da5669c","prod_name":"iPhone 17","prod_desc":"The latest Apple product in 2026","prod_price":999,"updated_at":"2025-05-04T01:42:39.750Z","createdAt":"2025-05-04T01:42:39.752Z","updatedAt":"2025-05-04T01:42:39.752Z","__v":0
To test updating and deleting a product by ID with curl:
curl -X PUT http://localhost:3000/api/products/6816c60f1812ad3e4da5669c \
-H "Content-Type: application/json" \
-d '{"prod_name": "iPhone 17 Pro", "prod_desc": "The latest Apple product in 2026", "prod_price": 1299}'
curl -X DELETE http://localhost:3000/api/products/6816c60f1812ad3e4da5669c
With your routes set up, your REST API is now fully functional and ready to be used by front-end apps, mobile clients, or even other services.
Error Handling and Validation (Updated for 2025)
A solid API must respond with clear error messages and validate incoming data to prevent bad inputs and bugs. In this section, we'll add:
- Basic request validation
- Centralized error handling
- Mongoose error awareness
1. Add Basic Validation in the Mongoose Schema
Let’s make sure required fields are enforced and have sensible rules in `models/Product.js`:
const productSchema = new mongoose.Schema(
{
prod_name: {
type: String,
required: [true, "Product name is required"],
trim: true,
minlength: [3, "Product name should be at least 10 characters"]
},
prod_desc: {
type: String,
required: [true, "Product description is required"],
minlength: [10, "Product description should be at least 10 characters"]
},
prod_price: {
type: Number,
required: [true, "Product price is required"],
min: [1, "Zero price not allowed"],
max: [1000, "Price too high"]
},
updated_at: { type: Date, default: Date.now }
},
{ timestamps: true }
);
✅ This will trigger validation errors if bad data is submitted when creating or updating tutorials.
2. Improve Validation Handling in Controllers
In productController.js, make sure you're catching Mongoose validation errors and returning readable messages.
For example, in productController and updateProduct, do this:
catch (err) {
if (err.name === 'ValidationError') {
const messages = Object.values(err.errors).map(val => val.message);
return res.status(400).json({ errors: messages });
}
res.status(500).json({ error: err.message });
}
Apply this pattern to all relevant try...catch blocks for consistent error responses.
3. Add a Central Error Handler (Optional but recommended)
For cleaner code, you can extract error handling to middleware. First, create a new file: middleware/errorHandler.js.
// middleware/errorHandler.js
export const errorHandler = (err, req, res, next) => {
console.error(err.stack);
const statusCode = res.statusCode !== 200 ? res.statusCode : 500;
res.status(statusCode).json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack,
});
};
Then, in server.js, use this middleware after all routes:
import { errorHandler } from './middleware/errorHandler.js';
// Register routes here...
app.use(errorHandler);
💡 This will catch and format any unhandled errors in your app.
4. Optional: Use express-validator for Request Body Validation
For more complex validation needs, consider using express-validator.
npm install express-validator
Then use it in your routes like this:
import express from "express";
import {
createProduct,
getAllProducts,
getProductById,
updateProduct,
deleteProduct
} from "../controllers/productController.js";
import { body } from "express-validator";
import { validateRequest } from "../middleware/validateRequest.js";
const router = express.Router();
router.post(
"/products",
[
body("prod_name").notEmpty().withMessage("Product name is required"),
body("prod_desc")
.isLength({ min: 10 })
.withMessage("Product desctiption must be at least 10 characters")
],
validateRequest,
createProduct
);
router.get("/products", getAllProducts);
router.get("/products/:id", getProductById);
router.put("/products/:id", updateProduct);
router.delete("/products/:id", deleteProduct);
export default router; body("prod_desc")
.isLength({ min: 10 })
.withMessage("Product desctiption must be at least 10 characters")
],
validateRequest,
createProduct
);
router.get("/products", getAllProducts);
router.get("/products/:id", getProductById);
router.put("/products/:id", updateProduct);
router.delete("/products/:id", deleteProduct);
export default router;
And create a middleware middleware/validateRequest.js to catch and return errors:i
mport { validationResult } from 'express-validator';
export const validateRequest = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array().map(e => e.msg) });
}
next();
};
With validation and error handling in place, your API is now safer, more professional, and easier to maintain.
Deploying to Production
Now that your REST API is complete, tested, and robust, it’s time to deploy it so others can access it over the internet. We'll use **Render** as a free and easy-to-use platform, but you can also use platforms like **Railway**, **Fly.io**, **Heroku**, or **VPS** servers.
1. Prepare the Project for Deployment
a. Create a `start` script in `package.json`
Update your `package.json` to include:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
b. Use environment variables for sensitive data
Replace hardcoded MongoDB connection strings with an environment variable.
In your server.js:
const mongoURI = process.env.MONGO_URI;
mongoose.connect(mongoURI, { ... });
Update your .env file:
PORT=3000
MONGO_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/yourdb
⚠️ Make sure .env is in .gitignore.
2. Push Code to GitHub
If not already done, initialize a git repo and push to GitHub:
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/yourusername/your-repo.git
git push -u origin main
3. Deploy to Render
1. Go to https://render.com and sign in.
2. Click “New Web Service”.
3. Connect your GitHub repo.
4. Choose:
- Environment: Node
- Build Command: npm install
- Start Command: npm start
- Environment Variables: Add PORT, MONGO_URI, etc.
- Click Deploy.
5. Render will build and host your app at a live URL like:
https://your-api.onrender.com
4. (Optional) Enable CORS for a Frontend App
If your API will be consumed by a frontend (e.g., React, Angular), install and configure CORS:
npm install cors
In your server.js, add:
import cors from 'cors';
app.use(cors());
5. Tips for Production
- Set NODE_ENV=production for optimized performance.
- Use a logging tool like Morgan or Winston for better observability.
- Monitor uptime using services like UptimeRobot or Better Stack.
- Use a rate limiter like express-rate-limit to protect against abuse.
Congratulations! 🎉 Your REST API is now live and ready for the world to use.
Conclusion
In this tutorial, you learned how to build a modern RESTful API using the latest versions of Node.js, Express.js, and MongoDB with Mongoose. We followed best practices, added a clear structure using controllers and routes, implemented robust error handling and validation, and deployed the final product to a live server.
With this API, you're ready to:
- Connect to a frontend (React, Angular, Vue, etc.)
- Integrate with mobile apps or third-party services
- Scale and extend with authentication, pagination, and more
This tutorial forms the foundation for many real-world applications.
🔗 GitHub Repository
You can find the full updated source code on GitHub:
👉 https://github.com/didinj/node-express-mongodb-restapi
Feel free to fork, star, or contribute to the project.
If you enjoyed this tutorial or found it helpful, consider sharing it or following Djamware.com for more modern full-stack web development content!
That is just the basics. If you need more deep learning about JavaScript, Node.js, Express.js, MongoDB, or related, you can take the following cheap course
- Node.js Express Api Rest Formation Complete
- Building APIs doing Outside-In TDD in Node and TypeScript
- Node JS
- JavaScript and Node.js Essentials for Test Automation
- Express.j,s Node.js, & MongoDB
- Build Role-based Authentication using Node.js, JWT, Mongoose
Happy coding! 🚀