Master Express.js development with these essential tips and best practices used by top developers in Europe.
Organize your Express application by splitting routes into separate modules. This improves maintainability and makes your codebase easier to navigate as it grows.
// routes/users.js
const router = require('express').Router();
router.get('/', getUsers);
router.post('/', createUser);
router.get('/:id', getUserById);
module.exports = router;
// app.js
app.use('/api/users', require('./routes/users'));Use middleware to handle authentication, logging, error handling, and request validation. This keeps your route handlers clean and focused on business logic.
const authenticate = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
req.user = await verifyToken(token);
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
app.use('/api/protected', authenticate);Create a wrapper function to handle async errors automatically. This eliminates repetitive try-catch blocks and ensures all errors are properly caught.
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage
router.get('/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json(users);
}));
// Error handler middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});Always validate incoming data to prevent security vulnerabilities and ensure data integrity. Express-validator provides a clean, declarative API for validation.
const { body, validationResult } = require('express-validator');
const validateUser = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('name').trim().notEmpty(),
];
router.post('/users', validateUser, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Create user...
});Use Helmet for security headers, implement CORS properly, and rate limit your endpoints. These are essential for production-ready applications.
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
app.use(helmet());
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true
}));
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', limiter);Load configuration from environment variables and create environment-specific settings. Never hardcode secrets or environment-specific values.
// config/index.js
require('dotenv').config();
module.exports = {
port: process.env.PORT || 3000,
db: {
url: process.env.DATABASE_URL,
options: {
maxPoolSize: parseInt(process.env.DB_POOL_SIZE) || 10
}
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '1d'
}
};Use structured logging with request IDs to trace requests across your system. This is invaluable for debugging production issues.
const { v4: uuid } = require('uuid');
const pino = require('pino-http');
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] || uuid();
res.setHeader('x-request-id', req.id);
next();
});
app.use(pino({
genReqId: (req) => req.id,
serializers: {
req: (req) => ({
id: req.id,
method: req.method,
url: req.url
})
}
}));Implement graceful shutdown to properly close database connections and finish processing requests before the server stops.
const server = app.listen(port);
const shutdown = async () => {
console.log('Shutting down gracefully...');
server.close(async () => {
await mongoose.connection.close();
await redis.quit();
console.log('Server closed');
process.exit(0);
});
setTimeout(() => {
console.error('Forced shutdown');
process.exit(1);
}, 10000);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);Use compression middleware to reduce response sizes. This significantly improves performance, especially for JSON-heavy APIs.
const compression = require('compression');
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
threshold: 1024 // Only compress if > 1KB
}));TypeScript provides type safety, better IDE support, and catches errors at compile time. It's especially valuable for larger Express applications.
import { Request, Response, NextFunction } from 'express';
interface User {
id: string;
email: string;
name: string;
}
interface AuthRequest extends Request {
user?: User;
}
const getUserProfile = async (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
const user = req.user;
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
res.json({ profile: user });
};Slashdev.io provides top Express.js developers for your projects in Europe and worldwide.
Hire Express.js Developers