| name | express |
| description | [Applies to: **/*.{js,ts}] This guide defines definitive best practices for building robust, scalable, and secure Express.js applications, emphasizing modern patterns and common pitfalls. |
| source | cursor_mdc |
express Best Practices
Express.js is the backbone of many Node.js backends. To build scalable, maintainable, and secure APIs in 2025, you must adhere to strict coding standards. This guide provides the definitive rules for our team.
1. Code Organization and Structure
Adopt a modular, layered architecture. This is non-negotiable for maintainability.
1.1. Enforce a Strict Modular Folder Structure
Separate concerns into dedicated directories.
❌ BAD: Monolithic server.js
const express = require('express');
const app = express();
const mongoose = require('mongoose');
app.get('/users', async (req, res) => { });
mongoose.connect('...');
app.listen(3000);
✅ GOOD: Layered Structure
📁 src/
├── config/ # Environment variables, DB connection
├── controllers/ # Request handling, orchestrates services
├── models/ # Mongoose schemas, data access
├── routes/ # API endpoint definitions
├── middlewares/ # Reusable Express middleware
├── services/ # Core business logic
├── utils/ # Helper functions
├── app.js # Express app setup, middleware, route registration
└── server.js # Server start, DB connection, graceful shutdown
📁 .env # Environment variables
1.2. Separate app.js from server.js
app.js configures the Express application. server.js starts the HTTP server and handles infrastructure (DB connection, graceful shutdown). This enables easier testing and deployment.
src/app.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const userRoutes = require('./routes/userRoutes');
const { notFound, errorHandler } = require('./middlewares/errorMiddleware');
const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use('/api/v1/users', userRoutes);
app.use(notFound);
app.use(errorHandler);
module.exports = app;
src/server.js
require('dotenv').config();
const app = require('./app');
const connectDB = require('./config/db');
const { port } = require('./config/config');
connectDB();
const server = app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
process.on('unhandledRejection', (err, promise) => {
console.error(`Error: ${err.message}`);
server.close(() => process.exit(1));
});
2. Environment Management
Never hardcode sensitive information.
2.1. Use .env for Configuration and Secrets
Store all configuration and secrets in .env files and load them with dotenv.
❌ BAD: Hardcoded secrets
const DB_URI = 'mongodb://user:password@localhost:27017/mydb';
const JWT_SECRET = 'supersecretkey';
✅ GOOD: .env and dotenv
.env
PORT=5000
MONGO_URI=mongodb://user:password@localhost:27017/mydb
JWT_SECRET=a_very_secure_random_string_for_jwt
src/config/config.js
require('dotenv').config();
module.exports = {
port: process.env.PORT || 3000,
mongoURI: process.env.MONGO_URI,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development',
};
3. Common Patterns and Anti-patterns
Embrace modern JavaScript and robust patterns.
3.1. Always Use async/await for Asynchronous Operations
Avoid callback hell and .then() chains.
❌ BAD: Callback hell or .then() chains
exports.getUsers = (req, res) => {
User.find().then(users => {
res.json(users);
}).catch(err => {
res.status(500).json({ message: err.message });
});
};
✅ GOOD: async/await with express-async-handler
Install npm i express-async-handler. This eliminates repetitive try/catch blocks in controllers.
src/controllers/userController.js
const asyncHandler = require('express-async-handler');
const userService = require('../services/userService');
exports.getUsers = asyncHandler(async (req, res) => {
const users = await userService.getAllUsers();
res.json(users);
});
exports.createUser = asyncHandler(async (req, res) => {
const newUser = await userService.createUser(req.body);
res.status(201).json(newUser);
});
4. Security Best Practices
Security is paramount. Implement these from day one.
4.1. Validate and Sanitize All User Input
Never trust client-side data. Use libraries like Joi or zod.
❌ BAD: No input validation
exports.createUser = asyncHandler(async (req, res) => {
const newUser = new User(req.body);
await newUser.save();
res.status(201).json(newUser);
});
✅ GOOD: Input Validation with Joi
Install npm i joi.
src/validation/userValidation.js
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
});
exports.validateUser = (req, res, next) => {
const { error } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ message: error.details[0].message });
}
next();
};
src/routes/userRoutes.js
const router = require('express').Router();
const { createUser } = require('../controllers/userController');
const { validateUser } = require('../validation/userValidation');
router.post('/', validateUser, createUser);
4.2. Use Security Middleware (helmet, cors, express-rate-limit)
Protect your API from common web vulnerabilities.
src/app.js (already shown, but reiterating)
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(helmet());
app.use(cors({ origin: process.env.CORS_ORIGIN || '*', credentials: true }));
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests from this IP, please try again after 15 minutes',
}));
4.3. Secure Cookies
Always set httpOnly, secure, and SameSite attributes for session cookies.
❌ BAD: Insecure cookies
res.cookie('token', token);
✅ GOOD: Secure cookies
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
maxAge: 3600000,
});
5. Error Handling
Centralize and standardize error responses.
5.1. Implement Centralized Error Handling Middleware
Catch all errors and return consistent JSON responses.
src/middlewares/errorMiddleware.js
const { nodeEnv } = require('../config/config');
const notFound = (req, res, next) => {
const error = new Error(`Not Found - ${req.originalUrl}`);
res.status(404);
next(error);
};
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
res.status(statusCode);
res.json({
message: err.message,
stack: nodeEnv === 'production' ? '🥞' : err.stack,
});
};
module.exports = { notFound, errorHandler };
src/app.js (already shown, but reiterating)
app.use(notFound);
app.use(errorHandler);
6. API Design
Design APIs for clarity, consistency, and future growth.
6.1. Follow RESTful Principles and Version Your API
Use clear resource-based URLs and HTTP methods. Versioning prevents breaking changes.
❌ BAD: Inconsistent, unversioned endpoints
app.get('/getAllUsers', );
app.post('/addUser', );
✅ GOOD: RESTful and Versioned
router.get('/', getUsers);
router.post('/', createUser);
router.get('/:id', getUserById);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
src/app.js
app.use('/api/v1/users', userRoutes);
7. Performance Considerations
Optimize for speed and efficiency.
7.1. Enable Gzip/Brotli Compression
Reduce response payload size.
Install npm i compression.
src/app.js
const express = require('express');
const compression = require('compression');
const app = express();
app.use(compression());
8. Testing Approaches
Ensure reliability and prevent regressions.
8.1. Implement Unit and Integration Tests
Use Jest and Supertest for comprehensive testing.
Install npm i --save-dev jest supertest.
package.json
{
"scripts": {
"test": "jest --detectOpenHandles"
}
}
src/tests/user.test.js
const request = require('supertest');
const app = require('../app');
const mongoose = require('mongoose');
const User = require('../models/User');
const { mongoURI } = require('../config/config');
beforeAll(async () => {
await mongoose.connect(mongoURI);
});
afterEach(async () => {
await User.deleteMany({});
});
afterAll(async () => {
await mongoose.connection.close();
});
describe('User API', () => {
it('should create a new user', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({
username: 'testuser',
email: 'test@example.com',
password:
});
(res.).();
(res.).(, );
});
(, () => {
(app).().({ : , : , : });
(app).().({ : , : , : });
res = (app).();
(res.).();
(res..).();
});
});