Express.js is one of the most popular frameworks for building web applications with Node.js. It's fast, minimal, and incredibly flexible. However, this flexibility often leaves room for mistakes, especially for developers new to backend development. In this post, we’ll cover some of the most common bad practices in Express.js, why they’re problematic, and how you can avoid them.
Let’s jump in! 🚀
🚨 1. Not Validating User Input
The Problem:
Failing to validate and sanitize user input can lead to serious vulnerabilities like SQL Injection or Cross-Site Scripting (XSS).
Example:
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Directly trust user input (Dangerous!)
authenticate(username, password);
res.send('Logged in');
});
Better Practice:
Always validate and sanitize incoming data. Use libraries like express-validator
.
const { body, validationResult } = require('express-validator');
app.post('/login', [
body('username').isString(),
body('password').isLength({ min: 6 })
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { username, password } = req.body;
authenticate(username, password);
res.send('Logged in');
});
Why?
Validating input prevents malicious data from causing harm to your application.
🚨 2. Not Handling Errors Properly
The Problem:
Uncaught errors can crash your app, exposing users to downtime and bad experiences.
Example:
app.get('/user/:id', (req, res) => {
const user = getUserById(req.params.id); // Might throw an error
res.send(user);
});
Better Practice:
Use an error-handling middleware to catch and process errors.
app.get('/user/:id', (req, res, next) => {
try {
const user = getUserById(req.params.id);
res.send(user);
} catch (error) {
next(error);
}
});
// Error-handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
Why?
Proper error handling keeps your app stable and prevents it from crashing unexpectedly.
🚨 3. Exposing Sensitive Information
The Problem:
Exposing stack traces, error details, or API keys in your responses can lead to serious security vulnerabilities.
Example:
app.get('/error', (req, res) => {
throw new Error('Sensitive error!');
});
Better Practice:
In production, avoid showing detailed error messages and sensitive information.
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Internal Server Error');
});
Why?
Hiding error details prevents attackers from gaining insights into your system.
🚨 4. Blocking the Event Loop
The Problem:
Performing heavy computations or synchronous tasks blocks the event loop, freezing your entire app.
Example:
app.get('/heavy-task', (req, res) => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
res.send(`Sum: ${sum}`);
});
Better Practice:
Offload heavy tasks to worker threads or use background services.
app.get('/heavy-task', (req, res) => {
// Use async/await or worker threads
doHeavyTask()
.then(result => res.send(`Result: ${result}`))
.catch(err => res.status(500).send('Failed to process'));
});
Why?
Blocking the event loop means no other requests can be processed until the current task is finished.
🚨 5. Hardcoding Configuration Values
The Problem:
Storing sensitive data (e.g., database credentials, API keys) directly in your code is a huge security risk.
Example:
const dbPassword = 'supersecret';
const dbHost = 'localhost';
Better Practice:
Store sensitive data in environment variables and use a library like dotenv
.
require('dotenv').config();
const dbPassword = process.env.DB_PASSWORD;
const dbHost = process.env.DB_HOST;
Why?
Environment variables keep sensitive data out of your codebase.
🚨 6. Not Using Middleware Properly
The Problem:
Skipping middleware like body parsers or rate limiters can make your app vulnerable to attacks.
Example:
app.post('/data', (req, res) => {
console.log(req.body); // Might be undefined without proper middleware
});
Better Practice:
Use built-in or third-party middleware appropriately.
const express = require('express');
const app = express();
app.use(express.json()); // Parses JSON requests
Why?
Middleware ensures requests are properly formatted and helps prevent abuse.
🚨 7. Poor API Design
The Problem:
Inconsistent naming, unclear endpoints, and messy response structures make APIs hard to use and maintain.
Example:
app.get('/getuser', (req, res) => {
res.send('User data');
});
Better Practice:
Follow RESTful conventions and standardize your API design.
app.get('/api/users/:id', (req, res) => {
res.json({ id: req.params.id, name: 'John Doe' });
});
Why?
A well-structured API is easier for both developers and clients to understand.
🚨 8. Not Securing Your App
The Problem:
Ignoring basic security practices (e.g., rate limiting, helmet, or CORS) leaves your app vulnerable.
Better Practice:
Use security middleware like helmet
and express-rate-limit
.
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
app.use(helmet());
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // Limit each IP to 100 requests per windowMs
}));
Why?
Security middleware protects your app from common attacks.
🚨 Conclusion
Building an Express.js app is fun, but small mistakes can turn into big problems if left unchecked. By following these best practices, you’ll create cleaner, safer, and more maintainable applications.
🛡️ Remember: Secure your app, validate your inputs, and handle errors gracefully.
What are some bad practices you’ve encountered in Express.js? Share them in the comments below! 🗨️
Happy coding! 💻✨