How to Build REST APIs with Node.js and Express
Why Node.js and Express Are the Go-To Stack for REST API Development
Node.js has become one of the most popular runtimes for server-side development, and for good reason. Its non-blocking, event-driven architecture makes it exceptionally well-suited for handling concurrent HTTP requests — exactly what REST API development demands. When you pair Node.js with Express, a minimal and flexible web framework, you get a productive environment that lets you define routes, handle middleware, and respond to clients with very little boilerplate.
Express doesn't impose rigid structure on your project, which makes it approachable for beginners while remaining powerful enough for production-grade software engineering. Whether you're building a simple CRUD API or a complex microservice, this stack scales with your needs.
Setting Up Your Project
Start by initializing a new Node.js project. Open your terminal and run the following commands:
mkdir my-api && cd my-api
npm init -y
npm install express
This creates a package.json and installs Express as a dependency. Next, create your entry file index.js. For development, install nodemon so the server restarts automatically on file changes:
npm install --save-dev nodemon
Add a start script to your package.json: "dev": "nodemon index.js". You're now ready to write your first route.
Creating Your First Express Server and Routes
A minimal Express server looks like this:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
The express.json() middleware parses incoming JSON request bodies, which is essential for POST and PUT endpoints. Routes are defined using HTTP method functions: app.get(), app.post(), app.put(), and app.delete(). Each maps a URL pattern to a handler function that receives the request and response objects.
Organizing Routes with Express Router
As your REST API development project grows, keeping all routes in a single file becomes unmanageable. Express's built-in Router lets you split routes into separate modules. Create a routes/users.js file:
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => res.json({ users: [] }));
router.post('/', (req, res) => {
const { name, email } = req.body;
res.status(201).json({ id: 1, name, email });
});
router.get('/:id', (req, res) => {
res.json({ id: req.params.id });
});
module.exports = router;
Then mount it in index.js with app.use('/api/users', require('./routes/users')). This modular approach is a cornerstone of clean software engineering and keeps your codebase maintainable as it scales.
Writing Middleware for Authentication and Error Handling
Middleware functions are the backbone of Express applications. They execute sequentially and can modify the request/response cycle or terminate it. A simple token-check middleware looks like this:
function authenticate(req, res, next) {
const token = req.headers['authorization'];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
next();
}
router.get('/protected', authenticate, (req, res) => {
res.json({ data: 'secret' });
});
For centralized error handling, define a four-argument middleware at the end of your middleware stack in index.js:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
Always pass errors to next(err) from your route handlers to trigger this handler consistently across your API.
Connecting to a Database
Most real-world REST APIs persist data. Mongoose is the most popular ODM for MongoDB with Node.js. Install it with npm install mongoose, then connect in your entry file:
const mongoose = require('mongoose');
mongoose.connect(process.env.MONGO_URI)
.then(() => console.log('DB connected'))
.catch(err => console.error(err));
Define a schema and model for your resource, then use Mongoose methods like Model.find(), Model.create(), and Model.findByIdAndUpdate() inside your route handlers. If you prefer SQL, libraries like pg for PostgreSQL or Sequelize as an ORM integrate just as cleanly with Express.
Best Practices for Production-Ready REST API Development
Following established conventions separates hobbyist projects from professional developer tools. Keep these practices in mind before deploying:
Use environment variables via the dotenv package for secrets, database URIs, and port numbers — never hardcode them. Validate input using a library like joi or express-validator to prevent malformed data from reaching your database. Version your API by prefixing routes with /api/v1/ so future breaking changes don't disrupt existing clients.
Enable CORS with the cors package to control which origins can access your API, and add rate limiting with express-rate-limit to protect against abuse. Finally, write integration tests using supertest alongside a testing framework like Jest — a habit that aligns with the broader discipline of test-driven development and ensures your endpoints behave correctly as the codebase evolves.
With these foundations in place, you have a solid, scalable architecture for REST API development that reflects real-world software engineering standards and prepares you for building production services.