Building REST APIs with Node.js: A Practical Guide for Beginners
If you are building a modern web or mobile application, chances are you need a backend API. REST APIs remain the most widely used approach for client-server communication, and Node.js with Express is one of the fastest ways to build one.
This guide walks you through everything you need to know to build a production-ready REST API from scratch.
What Is a REST API?
REST (Representational State Transfer) is an architectural style for designing networked applications. A REST API exposes data and functionality as resources, each identified by a URL, and uses standard HTTP methods to perform operations on them.
Key principles of REST:
- Statelessness — Each request from the client contains all the information the server needs. No session state is stored on the server between requests.
- Resource-based URLs — Endpoints represent resources (nouns), not actions (verbs). Use
/usersinstead of/getUsers. - Standard HTTP methods — Use GET, POST, PUT, PATCH, and DELETE for CRUD operations.
- JSON responses — Modern REST APIs almost always use JSON as the data format.
HTTP Methods Explained
| Method | Purpose | Example Endpoint | Body? |
|---|---|---|---|
| GET | Read/retrieve a resource | GET /api/users | No |
| POST | Create a new resource | POST /api/users | Yes |
| PUT | Replace an entire resource | PUT /api/users/5 | Yes |
| PATCH | Update part of a resource | PATCH /api/users/5 | Yes |
| DELETE | Remove a resource | DELETE /api/users/5 | No |
Important: GET requests should never modify data. POST is for creation. PUT replaces the entire resource while PATCH updates only the specified fields.
Setting Up Your Project
Start by initializing a new Node.js project and installing Express:
mkdir my-api && cd my-api
npm init -y
npm install express dotenv cors helmet
npm install -D nodemon
Create a basic server in server.js:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Routes
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Routing and Controllers
As your API grows, you should separate routes into their own files. A clean project structure looks like this:
my-api/
src/
controllers/
userController.js
middleware/
auth.js
errorHandler.js
routes/
userRoutes.js
models/
User.js
utils/
validators.js
server.js
.env
package.json
Here is an example route file for users:
// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { getUsers, getUserById, createUser, updateUser, deleteUser }
= require('../controllers/userController');
router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', createUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
module.exports = router;
Middleware: The Secret Weapon of Express
Middleware functions execute during the request-response cycle. They can modify the request, send a response, or pass control to the next middleware.
Common uses of middleware:
- Authentication — Verify JWT tokens before allowing access to protected routes
- Logging — Log every incoming request for debugging and monitoring
- Validation — Check that request bodies contain required fields
- Error handling — Catch and format errors consistently
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(403).json({ error: 'Invalid or expired token.' });
}
};
module.exports = authenticate;
Request Validation
Never trust incoming data. Always validate request bodies before processing them. A popular library for this is Joi:
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
role: Joi.string().valid('student', 'instructor', 'admin').default('student')
});
Validation prevents malformed data from reaching your database and protects against injection attacks.
Error Handling
A centralized error handler keeps your code clean and your API responses consistent:
// src/middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
const status = err.statusCode || 500;
res.status(status).json({
error: {
message: err.message || 'Internal Server Error',
status,
}
});
};
module.exports = errorHandler;
Always return proper HTTP status codes: 200 for success, 201 for created, 400 for bad request, 401 for unauthorized, 404 for not found, and 500 for server errors.
Authentication with JWT
JSON Web Tokens are the standard for stateless API authentication. The flow works like this:
- User sends login credentials (email and password) to
POST /api/auth/login - Server verifies credentials and returns a signed JWT
- Client stores the token and sends it in the
Authorizationheader with every subsequent request - Server middleware verifies the token on protected routes
Security tip: Store JWTs in httpOnly cookies for web applications, not in localStorage. Set short expiration times (15-30 minutes) and use refresh tokens for longer sessions.
Best Practices for Production APIs
- Use environment variables for secrets, database URLs, and configuration. Never hardcode them.
- Implement rate limiting using
express-rate-limitto prevent abuse. - Add request logging with libraries like
morganorwinston. - Version your API — prefix all routes with
/api/v1/so you can introduce breaking changes safely. - Use HTTPS in production. Tools like Let's Encrypt make this free.
- Write tests — Use Jest and Supertest to test your endpoints automatically.
- Document your API — Swagger (OpenAPI) generates interactive documentation from your code.
What to Build Next
The best way to learn is to build a real project. Here are some ideas:
- Task Manager API — CRUD for tasks with user authentication and categories
- Blog API — Posts, comments, and user roles (author, editor, admin)
- E-commerce API — Products, cart, orders, and payment integration
Each of these will force you to deal with relationships between resources, authentication, pagination, and error handling — all skills that are essential for backend roles.
Final Thoughts
Node.js and Express give you a fast, flexible, and well-supported foundation for building REST APIs. The ecosystem is mature, the community is massive, and the job market for Node.js developers in India continues to grow.
Start with the basics, follow the project structure outlined here, and gradually add complexity. Every production API you encounter in your career will use these same patterns.