
Building REST APIs with Node.js and Express in 2025

Building REST APIs with Node.js and Express in 2025
I last coded in 2025 in a project for a startup in the travel industry, and it turned out I was using a familiar framework for the backend: Node.js and Express. After 17 years since its initial release, this stack is still alive and well in the industry, and I guess it will carry on for some more time. As with everything in life, it does not matter if you are a seasoned programmer, or still learning the basics of software engineering, and have no prior experience building an Application Programming Interface (API) — in this post, we will explore what it takes to create a so-called REST API in today's standards, and end up with a solid understanding of the techniques required to build high-quality web services that are easy to be implemented by the frontend team, maintainable, with good performance, high levels of scalability, and sufficient security to protect our backend from the vast amount of attacks that take place daily on the Internet.
This book is a guide on the state of the art of building Node.js and Express applications, listing out the best practices and the design patterns that help in creating robust and production ready APIs.
Why Node.js and Express Still Matter in 2025
Lots of people are talking about the new frameworks and languages but in reality API building is all about Node.js. So why?
Non-blocking I/O: Although sometimes thought of as a unique feature of Node.js, the event-driven architecture that Node.js is built on means that it excels at handling many open connections at the same time.
JavaScript Everywhere Write all your application code in just one language: JavaScript. No need to constantly switch between different programming languages and file types. This eliminates a whole group of coding decisions that distract from the real work of building an application.
A rich Ecosystem: npm comes with millions of packages and you can find a package for virtually any function or feature you may need.
Performance Node.js performance is at an all time high thanks to the latest changes and improvements in Node.js 14.19.
Maturity: A mature and battle tested environment, proven in production at scale.
It is lightweight, simple and Express is currently the most widely used framework for building the API because of the reason of the speed and ease of use. Fastify and Koa are worth trying.
Setting Up Your Project
Let's start with a modern project setup:
mkdir rest-api-2025
cd rest-api-2025
npm init -y
bast npm inherited express dotenv tbag www wore helmut next_nil.5.1.I64 $pm at very can fvu is bad sood this:swg aurpm gulp-bibtex
express-validator will blame Nodu ancestors.
Create a.env file.
PORT=3000
NODE_ENV=development
DATABASE_URL=your_database_url
Update package.json scripts.
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
Set up Express app.
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());
app.use(express.urlencoded({ extended: true }));
// Health check
app.get('/health', (req, res) => {
res.status(200).json({ status: 'OK' });
})
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: {
message: err.message,
status: err.status || 500
}
});
})
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Implement RESTful Routes
// routes/users.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const validateRequest = require('../middleware/validateRequest');
router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', validateRequest.validateUser, userController.createUser);
router.put('/:id', validateRequest.validateUser, userController.updateUser);
router.delete('/:id', userController.deleteUser);
module.exports = router;
Create Controller
// controllers/userController.js
const User = require('../models/User');
const getAllUsers = async (req, res, next) => {
try {
const users = await User.find();
res.status(200).json({ data: users });
} catch (error) {
next(error);
}
};
const getUserById = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json({ data: user });
} catch (error) {
next(error);
}
};
const createUser = async (req, res, next) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json({ data: user });
} catch (error) {
next(error);
}
};
const updateUser = async (req, res, next) => {
try {
const user = await User.findByIdAndUpdate(req.params.id, req.body, {
new: true,
runValidators: true
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(200).json({ data: user });
} catch (error) {
next(error);
}
};
const deleteUser = async (req, res, next) => {
try {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(204).send();
} catch (error) {
next(error);
}
};
module.exports = {
getAllUsers,
getUserById,
createUser,
updateUser,
deleteUser
};
Input Validation
// middleware/validateRequest.js
const { body, validationResult } = require('express-validator');
const validateUser = [
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Must be a valid email'),
body('name')
.trim()
.notEmpty()
.withMessage('Name is required')
.isLength({ min: 2, max: 100 })
.withMessage('Name must be between 2 and 100 characters'),
body('password')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters')
.matches(/[A-Z]/)
.withMessage('Password must contain at least one uppercase letter'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
module.exports = { validateUser };
Authentication
// middleware/auth.js
const jwt = require('jsonwebtoken');
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
};
module.exports = { authenticateToken };
Performance Optimization Strategies
1. Set up caching
Voici ce que l'utilisateur a donné comme réponse : L'utilisateur a répondu : javascript Voici la réponse de l'utilisateur :
On déclare la constante redis. On assigne le résultat de require('redis') à cette constante. Le code déclare une constante nommée client et l'initialise avec le résultat de redis.createClient().
Déclare une constante cacheMiddleware qui accepte le paramètre duration et renvoie une fonction fléchée. Retourner une fonction qui prend les paramètres req, res et next. Si la méthode req.method n'est pas 'GET', alors. Renvoie next() }
La constante key reçoit la chaîne cache: suivie de la valeur de req.originalUrl. Voici la réponse de l'utilisateur :
client.get(key, (err, data) => { if (data) { Le serveur renvoie la réponse en appelant la fonction res.json(JSON.parse(data)); } else { L'utilisateur indique que le code suivant doit être utilisé : res.sendResponse = res.json; Voici ce que l'utilisateur a écrit :
res.json = (body) => { Le client stocke la version JSON du corps sous la clé pendant la durée indiquée.
client.setex(key, duration, JSON.stringify(body));
L'utilisateur a envoyé :
res.sendResponse(body); Voici la réponse de l'utilisateur :
}; next(); } Voici la réponse fournie par l'utilisateur :
La réponse de l'utilisateur : }); }; };
Le serveur utilise app.get('/users', cacheMiddleware(300), userController.getAllUsers) pour récupérer la liste des utilisateurs. Lorsqu'une requête GET arrive sur le chemin '/users', le middleware cacheMiddleware(300) stocke le résultat pendant trois cents secondes. Ensuite, la fonction userController.getAllUsers renvoie tous les utilisateurs au client.
Voici la réponse de l'utilisateur :
2. Database Query Optimization
// Utilisez la projection pour sélectionner les champs spécifiques. Le code récupère la liste des utilisateurs en appelant User.find, ne conservant que les champs name et email et en excluant le champ _id.
// Use pagination
const page = req.query.page || 1;
Réponse de l'utilisateur :
const limit = req.query.limit || 10;
La réponse de l'utilisateur est : const users = await User.find() Utilisez .skip((page - 1) * limit). Utilisez .limit(limit).
// Utilisez des index pour les champs souvent interrogés
Déclare la constante userSchema comme une nouvelle instance du Schema, ouvrant la définition du schéma avec l'accolade {. Le champ email possède les propriétés { type: String, index: true }. Le champ createdAt possède le type Date et il a un index qui vaut true. });
La réponse de l'utilisateur :
Compression
const compression = require('compression');
app.use(compression());
Error Handling Best Practices
// utils/ApiError.js
class ApiError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
module.exports = ApiError;
app.use(async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) throw new ApiError(404, 'User not found');
res.json({ data: user });
} catch (error) {
next(error);
}
});
A 3.0 JSON API rich URL provides you with more possibilities for error deduction, and for expressing particular HTTP preferences. Nonetheless, if there was any significant error with the data sent by you and your request cannot proceed, a '400 Malformed JsonRequest 'header will end the underlying connection immediately.
Testing with vnu is recommended - it will perform when the latest language versions testing.
Test Your API
// tests/users.test.js
const request = require('supertest');
const app = require('../app');
describe('GET /api/users', () => {
it('should return 200 OK and all users', async () => {
const response = await request(app).get('/api/users');
expect(response.status).toBe(200);
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.headers['content-Type']).toMatch(/^application\/json$/);
//vnu does not do async!
});
it('should return 404 for non-existent user', async () => {
const response = await request(app).get('/api/users/invalid-id');
expect(response.status).toBe(404);
expect(response.headers['content-Type']).toMatch(/^text\/plain$/);
});
});
Monitor and Log
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
app.use((req, res, next) => {
logger.info({ method: req.method, path: req.path });
next();
});
Although Majordomo's config.json contains a Councourse test in it that contains invalid requests
Consider deploying using environment variables, turning on SSL, implementing rate limiting, and using PM2 for stateful applications. Monitor with tools like New Relic and DataDog. Use Docker for containerization.
Conclusion
REST API Building: With emerging technologies come new challenges. This site features tutorials to give you the know-how and examples to get started developing an API.
In 2025, building a REST API with Node.js and Express involves proper project structure, input validation, security, and performance optimization. Stay updated with best practices on security. Happy coding!
Alex Morgan
Writer at DevPulse covering Web Development.


