Express.js is the most widely deployed Node.js web framework. Interviews test your understanding of middleware architecture, routing, async error handling, REST design, and security. This guide covers the 50 most common questions — with concise answers and code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Core concepts | What is Express, middleware chain, req/res cycle |
| Routing | Router, path params, query strings, nested routes |
| Middleware | Built-in, third-party, error-handling, order matters |
| REST APIs | CRUD patterns, status codes, response formats |
| Authentication | JWT, session-based, Passport.js |
| Error handling | Sync vs async errors, global handler |
| Performance | Compression, caching, clustering |
| Security | Helmet, CORS, rate limiting, input validation |
Core Concepts
1. What is Express.js and why use it?
Express is a minimal, unopinionated web framework for Node.js that adds routing, middleware support, and HTTP utilities on top of Node's built-in http module.
| Feature | Node http |
Express |
|---|---|---|
| Routing | Manual if (url === '/users') |
app.get('/users', handler) |
| Middleware | Roll your own | app.use(fn) chain |
| Request parsing | Manual Buffer assembly |
express.json() built-in |
| Response helpers | res.end(JSON.stringify(data)) |
res.json(data) |
| Error handling | Try/catch per handler | Centralised 4-arg handler |
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello, Express!'));
app.listen(3000, () => console.log('Listening on port 3000'));
2. Explain the Express request-response cycle.
- Incoming HTTP request hits Node's
http.Server - Express matches the path/method to a route
- The middleware stack executes left-to-right / top-to-bottom
- Each middleware calls
next()to pass control, or sends a response to end the cycle - A route handler sends the final response
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`); // middleware
next();
});
app.get('/users', (req, res) => {
res.json([{ id: 1, name: 'Alice' }]); // response — cycle ends
});
3. What is middleware in Express?
A middleware is a function with the signature (req, res, next). It can:
- Execute any code
- Modify
req/res - End the request-response cycle
- Call
next()to pass to the next middleware
// Logger middleware
const logger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
};
app.use(logger);
4. What are the types of middleware in Express?
| Type | How registered | Example |
|---|---|---|
| Application-level | app.use() / app.METHOD() |
logging, auth |
| Router-level | router.use() |
scoped to a prefix |
| Error-handling | app.use((err, req, res, next) => {}) |
global error handler |
| Built-in | express.json(), express.static() |
body parsing, static files |
| Third-party | npm install cors helmet |
CORS, security headers |
5. What does next() do? What is next(err)?
next() (no argument) passes control to the next matching middleware or route handler.
next(err) (with an argument) skips all remaining regular middleware and jumps directly to the error-handling middleware (4-argument form).
app.get('/user/:id', (req, res, next) => {
if (isNaN(req.params.id)) {
return next(new Error('Invalid ID')); // jump to error handler
}
res.json({ id: req.params.id });
});
// Error-handling middleware (must have 4 params)
app.use((err, req, res, next) => {
res.status(400).json({ error: err.message });
});
6. What is the difference between app.use() and app.get()?
app.use() |
app.get() |
|
|---|---|---|
| HTTP method | All methods | GET only |
| Path matching | Prefix match (/api matches /api/users) |
Exact match |
| Use case | Middleware, sub-routers | Route handlers |
app.use('/api', apiRouter); // mounts router at /api/*
app.get('/api/users', handler); // exact GET /api/users only
Routing
7. How do you define route parameters?
Use :paramName in the path. Access via req.params.
app.get('/users/:id', (req, res) => {
res.json({ userId: req.params.id });
});
// Optional param
app.get('/posts/:year/:month?', (req, res) => {
res.json(req.params); // { year: '2025', month: '07' }
});
8. What are query strings and how do you access them?
Query strings appear after ? in the URL (/search?q=express&page=2). Access via req.query.
app.get('/search', (req, res) => {
const { q = '', page = 1, limit = 10 } = req.query;
res.json({ query: q, page: Number(page), limit: Number(limit) });
});
9. What is express.Router()?
Router is a mini Express application — it has its own middleware stack and routes but no listen(). Use it to modularise routes.
// routes/users.js
const router = require('express').Router();
router.get('/', getAllUsers);
router.get('/:id', getUser);
router.post('/', createUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);
module.exports = router;
// app.js
const usersRouter = require('./routes/users');
app.use('/users', usersRouter); // all routes prefixed with /users
10. How do you handle multiple HTTP methods on the same path?
Use app.route() to chain methods:
app.route('/users/:id')
.get((req, res) => res.json(getUser(req.params.id)))
.put((req, res) => res.json(updateUser(req.params.id, req.body)))
.delete((req, res) => { deleteUser(req.params.id); res.status(204).end(); });
11. How do you serve static files in Express?
Use the built-in express.static() middleware:
// Serve files from the 'public' directory
app.use(express.static('public'));
// With virtual path prefix
app.use('/assets', express.static('public'));
// GET /assets/logo.png → ./public/logo.png
Request Parsing
12. How do you parse JSON request bodies?
Use the built-in express.json() middleware (replaces the deprecated body-parser):
app.use(express.json());
app.use(express.urlencoded({ extended: true })); // for form data
app.post('/users', (req, res) => {
console.log(req.body); // { name: 'Alice', email: 'alice@example.com' }
res.status(201).json(req.body);
});
13. What is the difference between req.params, req.query, and req.body?
req.params |
req.query |
req.body |
|
|---|---|---|---|
| Source | URL path | URL after ? |
Request body |
| Example URL | /users/42 |
/users?page=2 |
POST body |
| Middleware needed | None | None | express.json() |
| Type | String always | String/Array | Depends on body |
// GET /users/42?fields=name,email + POST body: {"role": "admin"}
app.put('/users/:id', (req, res) => {
const id = req.params.id; // '42'
const fields = req.query.fields; // 'name,email'
const data = req.body; // { role: 'admin' }
});
14. How do you handle file uploads in Express?
Use the multer middleware:
const multer = require('multer');
const upload = multer({
dest: 'uploads/',
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
fileFilter: (req, file, cb) => {
if (!file.mimetype.startsWith('image/')) {
return cb(new Error('Only images allowed'));
}
cb(null, true);
}
});
app.post('/avatar', upload.single('photo'), (req, res) => {
res.json({ filename: req.file.filename });
});
Response Methods
15. What response methods does Express provide?
| Method | Description |
|---|---|
res.send() |
Send string, Buffer, or object |
res.json() |
Send JSON + set Content-Type: application/json |
res.status(code) |
Set HTTP status code |
res.redirect(url) |
Redirect to URL (302 default) |
res.render(view) |
Render a template |
res.sendFile(path) |
Stream a file |
res.download(path) |
Trigger file download |
res.end() |
End response with no body |
res.set(header, value) |
Set response header |
res.cookie(name, val) |
Set cookie |
res.status(201).json({ id: 1, name: 'Alice' });
res.status(404).json({ error: 'Not found' });
res.redirect(301, 'https://example.com/new-url');
16. How do you set response headers in Express?
app.get('/data', (req, res) => {
res.set('X-Custom-Header', 'value');
res.set({
'Cache-Control': 'no-store',
'Content-Type': 'application/json'
});
res.json({ ok: true });
});
Error Handling
17. How do you handle errors in Express?
Define a 4-argument middleware (err, req, res, next) after all routes:
// Sync errors — throw inside handlers
app.get('/sync', (req, res) => {
throw new Error('Sync error'); // Express 5 catches automatically
});
// Async errors — pass to next(err)
app.get('/async', async (req, res, next) => {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
next(err);
}
});
// Error handler (must be last)
app.use((err, req, res, next) => {
console.error(err.stack);
const status = err.status || 500;
res.status(status).json({
error: {
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
});
18. What is the difference between Express 4 and Express 5 error handling?
| Express 4 | Express 5 | |
|---|---|---|
| Async errors | Must wrap in try/catch + next(err) |
async handlers auto-forward rejections |
next(err) still works |
Yes | Yes |
app.use(errorHandler) |
Yes | Yes |
| Release status | Stable | Stable (2024) |
Express 5 makes async error handling much simpler — thrown errors and rejected promises are automatically passed to error middleware.
19. How do you create a custom error class?
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Usage
app.get('/users/:id', async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) throw new AppError('User not found', 404);
res.json(user);
});
// Error handler checks isOperational
app.use((err, req, res, next) => {
if (err.isOperational) {
return res.status(err.statusCode).json({ error: err.message });
}
// Unknown error — don't leak details
console.error('UNEXPECTED:', err);
res.status(500).json({ error: 'Internal server error' });
});
20. How do you handle 404 routes in Express?
Add a catch-all middleware after all routes:
// Specific routes above...
app.get('/users', handler);
// 404 handler — no route matched
app.use((req, res, next) => {
next(new AppError(`Route ${req.method} ${req.url} not found`, 404));
});
// Error handler
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({ error: err.message });
});
Middleware Deep Dive
21. What is the order of middleware execution and why does it matter?
Express executes middleware in the order they are defined. A middleware registered after a route will not run for that route.
// CORRECT: auth runs before routes
app.use(authMiddleware);
app.get('/protected', handler);
// WRONG: auth never runs for /protected
app.get('/protected', handler);
app.use(authMiddleware); // too late
22. What is a parameterised middleware (middleware factory)?
A function that returns a middleware — useful for configuration:
const rateLimit = (windowMs, max) => {
const store = new Map();
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
const entry = store.get(key) || { count: 0, resetAt: now + windowMs };
if (now > entry.resetAt) { entry.count = 0; entry.resetAt = now + windowMs; }
entry.count++;
store.set(key, entry);
if (entry.count > max) return res.status(429).json({ error: 'Too Many Requests' });
next();
};
};
app.use('/api', rateLimit(60_000, 100)); // 100 req/min
23. How do you write an async middleware wrapper to avoid try/catch in every handler?
// Express 4 utility — wraps async handlers
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json(users);
}));
// No try/catch needed — errors automatically go to next(err)
Express 5 makes this unnecessary as async handlers auto-forward rejections.
24. What built-in middleware does Express 4 provide?
| Middleware | Purpose |
|---|---|
express.json() |
Parse application/json bodies |
express.urlencoded() |
Parse URL-encoded form bodies |
express.static() |
Serve static files |
express.raw() |
Parse raw binary bodies |
express.text() |
Parse plain text bodies |
REST API Design
25. What HTTP status codes should a REST API return?
| Scenario | Status Code |
|---|---|
| Successful GET | 200 OK |
| Resource created | 201 Created |
| No content (DELETE) | 204 No Content |
| Bad request | 400 Bad Request |
| Unauthenticated | 401 Unauthorized |
| Forbidden (no permission) | 403 Forbidden |
| Not found | 404 Not Found |
| Conflict (duplicate) | 409 Conflict |
| Validation error | 422 Unprocessable Entity |
| Rate limited | 429 Too Many Requests |
| Server error | 500 Internal Server Error |
26. How do you build a RESTful CRUD API in Express?
const router = require('express').Router();
const db = require('../db'); // your data layer
router.get('/', async (req, res, next) => {
try {
const items = await db.findAll();
res.json(items);
} catch (err) { next(err); }
});
router.get('/:id', async (req, res, next) => {
try {
const item = await db.findById(req.params.id);
if (!item) return res.status(404).json({ error: 'Not found' });
res.json(item);
} catch (err) { next(err); }
});
router.post('/', async (req, res, next) => {
try {
const item = await db.create(req.body);
res.status(201).json(item);
} catch (err) { next(err); }
});
router.put('/:id', async (req, res, next) => {
try {
const item = await db.update(req.params.id, req.body);
if (!item) return res.status(404).json({ error: 'Not found' });
res.json(item);
} catch (err) { next(err); }
});
router.delete('/:id', async (req, res, next) => {
try {
await db.delete(req.params.id);
res.status(204).end();
} catch (err) { next(err); }
});
module.exports = router;
27. How do you version your Express API?
Option 1: URL path versioning (most common)
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Option 2: Header versioning
app.use((req, res, next) => {
const version = req.headers['api-version'] || '1';
req.apiVersion = version;
next();
});
Option 3: Query string
// GET /api/users?version=2
URL versioning is preferred for simplicity and cacheability.
28. How do you implement pagination in Express?
app.get('/users', async (req, res, next) => {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, parseInt(req.query.limit) || 20);
const skip = (page - 1) * limit;
try {
const [users, total] = await Promise.all([
User.find().skip(skip).limit(limit),
User.countDocuments()
]);
res.json({
data: users,
pagination: {
page, limit, total,
totalPages: Math.ceil(total / limit),
hasNext: page < Math.ceil(total / limit),
hasPrev: page > 1
}
});
} catch (err) { next(err); }
});
Authentication
29. How do you implement JWT authentication in Express?
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
// Login endpoint
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ sub: user.id, role: user.role }, SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ sub: user.id }, SECRET, { expiresIn: '7d' });
res.json({ token, refreshToken });
});
// Auth middleware
const auth = (req, res, next) => {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' });
try {
req.user = jwt.verify(header.slice(7), SECRET);
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
};
// Protected route
app.get('/profile', auth, (req, res) => {
res.json({ userId: req.user.sub });
});
30. What is Passport.js and how does it integrate with Express?
Passport is an authentication middleware for Node.js that supports 500+ strategies (local, OAuth, JWT, SAML, etc.).
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
const user = await User.findOne({ email });
if (!user) return done(null, false, { message: 'User not found' });
if (!(await user.comparePassword(password))) {
return done(null, false, { message: 'Wrong password' });
}
done(null, user);
}
));
app.use(passport.initialize());
app.post('/login',
passport.authenticate('local', { session: false }),
(req, res) => res.json({ user: req.user })
);
31. How do you implement session-based authentication?
const session = require('express-session');
const RedisStore = require('connect-redis').default;
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 1 day
}
}));
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
req.session.userId = user.id;
res.json({ message: 'Logged in' });
});
const requireAuth = (req, res, next) => {
if (!req.session.userId) return res.status(401).json({ error: 'Unauthorized' });
next();
};
Security
32. How do you add security headers to an Express app?
Use helmet:
const helmet = require('helmet');
app.use(helmet()); // Sets 11 security headers including:
// Content-Security-Policy
// X-Frame-Options: SAMEORIGIN
// X-Content-Type-Options: nosniff
// Strict-Transport-Security (HSTS)
// Referrer-Policy
// Custom CSP
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'cdn.example.com'],
styleSrc: ["'self'", "'unsafe-inline'"],
}
}));
33. How do you configure CORS in Express?
const cors = require('cors');
// Allow all origins (not for production)
app.use(cors());
// Configured CORS
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // preflight cache 24h
};
app.use(cors(corsOptions));
34. How do you prevent SQL injection and XSS in Express?
SQL Injection — always use parameterised queries:
// WRONG — SQL injection vulnerable
const users = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// CORRECT — parameterised
const users = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
Input validation — use express-validator or zod:
const { body, validationResult } = require('express-validator');
app.post('/users',
body('email').isEmail().normalizeEmail(),
body('name').trim().isLength({ min: 2, max: 50 }),
body('age').isInt({ min: 0, max: 150 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
// safe to use req.body now
}
);
XSS — escape output or use a template engine that auto-escapes. Never insert untrusted HTML directly.
35. How do you implement rate limiting in Express?
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per window
message: { error: 'Too many requests, try again later.' },
standardHeaders: true, // Return X-RateLimit-* headers
legacyHeaders: false,
});
// Apply to all API routes
app.use('/api', limiter);
// Stricter limit for auth endpoints
const authLimiter = rateLimit({ windowMs: 60_000, max: 5 });
app.use('/auth', authLimiter);
Performance
36. How do you compress responses in Express?
const compression = require('compression');
app.use(compression({
threshold: 1024, // only compress responses > 1 KB
level: 6, // gzip compression level (1-9)
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
}
}));
37. How do you implement caching in Express?
HTTP caching with headers:
app.get('/static-data', (req, res) => {
res.set('Cache-Control', 'public, max-age=3600'); // cache 1 hour
res.json(staticData);
});
In-memory caching with node-cache:
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5 min TTL
const cacheMiddleware = (req, res, next) => {
const key = req.originalUrl;
const cached = cache.get(key);
if (cached) return res.json(cached);
const originalJson = res.json.bind(res);
res.json = (data) => {
cache.set(key, data);
originalJson(data);
};
next();
};
app.get('/users', cacheMiddleware, getAllUsers);
38. How do you use clustering to utilise multiple CPU cores?
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`Master ${process.pid} starting ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) cluster.fork();
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.pid} died — restarting`);
cluster.fork();
});
} else {
const app = require('./app');
app.listen(3000, () => console.log(`Worker ${process.pid} listening`));
}
In production, prefer PM2 (pm2 start app.js -i max) which handles clustering, restarts, and logging.
Testing
39. How do you test Express routes?
Use supertest to make HTTP requests to the app without starting a real server:
const request = require('supertest');
const app = require('../app');
describe('GET /users', () => {
it('returns a list of users', async () => {
const res = await request(app)
.get('/users')
.set('Authorization', `Bearer ${testToken}`);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
it('returns 401 without auth', async () => {
const res = await request(app).get('/users');
expect(res.status).toBe(401);
});
});
describe('POST /users', () => {
it('creates a user with valid data', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'Alice', email: 'alice@example.com' });
expect(res.status).toBe(201);
expect(res.body).toMatchObject({ name: 'Alice' });
});
});
40. How do you test middleware in isolation?
const authMiddleware = require('../middleware/auth');
describe('auth middleware', () => {
const mockReq = (headers = {}) => ({ headers });
const mockRes = () => {
const res = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
return res;
};
const mockNext = jest.fn();
it('calls next() with valid token', () => {
const req = mockReq({ authorization: `Bearer ${validToken}` });
const res = mockRes();
authMiddleware(req, res, mockNext);
expect(mockNext).toHaveBeenCalled();
expect(mockNext).not.toHaveBeenCalledWith(expect.any(Error));
});
it('returns 401 without token', () => {
const req = mockReq({});
const res = mockRes();
authMiddleware(req, res, mockNext);
expect(res.status).toHaveBeenCalledWith(401);
});
});
Advanced Topics
41. How do you structure a large Express application?
src/
├── app.js # Express setup, middleware registration
├── server.js # http.createServer, port binding
├── config/
│ ├── index.js # Environment config
│ └── database.js # DB connection
├── routes/
│ ├── index.js # Mount all routers
│ ├── users.js # User routes
│ └── products.js # Product routes
├── controllers/
│ ├── userController.js
│ └── productController.js
├── services/ # Business logic
├── middleware/
│ ├── auth.js
│ ├── validate.js
│ └── errorHandler.js
├── models/ # Database models
└── utils/ # Shared utilities
42. What is the difference between app.js and server.js?
Separating them enables testing without starting a server:
// app.js — Express setup only
const express = require('express');
const app = express();
app.use(express.json());
app.use('/users', require('./routes/users'));
app.use(require('./middleware/errorHandler'));
module.exports = app;
// server.js — starts listening
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
Supertest imports app.js directly without calling listen().
43. How do you implement graceful shutdown?
const server = app.listen(PORT);
const shutdown = async (signal) => {
console.log(`${signal} received — starting graceful shutdown`);
server.close(async () => {
await db.disconnect();
console.log('Server closed, process exiting');
process.exit(0);
});
// Force exit after 30s
setTimeout(() => process.exit(1), 30_000).unref();
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
44. How do you use template engines with Express?
// EJS
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/home', (req, res) => {
res.render('index', { title: 'My App', user: req.user });
});
// views/index.ejs: <h1><%= title %></h1> <p>Hello, <%= user.name %></p>
// Handlebars
const { engine } = require('express-handlebars');
app.engine('handlebars', engine());
app.set('view engine', 'handlebars');
45. How do you implement WebSockets alongside Express?
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: '*' } });
io.on('connection', (socket) => {
console.log(`Client connected: ${socket.id}`);
socket.on('message', (data) => {
io.emit('message', data); // broadcast to all
});
socket.on('disconnect', () => console.log(`Client disconnected: ${socket.id}`));
});
// Regular Express routes still work
app.get('/api/status', (req, res) => res.json({ ok: true }));
httpServer.listen(3000);
46. How do you handle environment variables in Express?
// .env
// PORT=3000
// DB_URL=mongodb://localhost:27017/mydb
// JWT_SECRET=supersecret
// Load at entry point (server.js)
require('dotenv').config();
// config/index.js — typed, validated config
const config = {
port: parseInt(process.env.PORT || '3000'),
db: {
url: process.env.DB_URL,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '15m'
},
isDev: process.env.NODE_ENV !== 'production'
};
// Fail fast if required env vars are missing
const required = ['DB_URL', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) throw new Error(`Missing required env var: ${key}`);
}
module.exports = config;
47. How do you implement request logging in Express?
Development: Use morgan:
const morgan = require('morgan');
app.use(morgan('dev')); // "GET /users 200 12ms"
Production: Use structured logging with winston or pino:
const pino = require('pino');
const logger = pino({ level: 'info' });
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now() - start,
ip: req.ip,
});
});
next();
});
48. What is express-validator and how does it work?
const { body, param, query, validationResult } = require('express-validator');
const validateCreateUser = [
body('name').trim().notEmpty().withMessage('Name is required')
.isLength({ max: 50 }).withMessage('Name max 50 chars'),
body('email').isEmail().normalizeEmail(),
body('age').optional().isInt({ min: 18, max: 100 }),
body('role').isIn(['user', 'admin']).withMessage('Invalid role'),
];
app.post('/users', validateCreateUser, (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
// req.body is validated and sanitised
});
Common Anti-Patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
| No error-handling middleware | Unhandled errors crash the process | Add 4-arg (err, req, res, next) handler last |
| Sending after response | Cannot set headers after they are sent crash |
Use return res.json(...) |
| Sync blocking in handlers | Blocks the event loop for all requests | Use async file I/O, non-blocking DB queries |
| Storing secrets in code | Exposes credentials in git | Use process.env + .env + dotenv |
| No input validation | SQL injection, XSS, logic bugs | Use express-validator or zod |
| Missing rate limiting | Brute-force attacks on auth | Add express-rate-limit to /auth routes |
Not using helmet |
Missing security headers | app.use(helmet()) |
Global app.use(cors()) |
Allows any origin | Configure specific allowed origins |
Express vs Other Frameworks
| Feature | Express | Fastify | NestJS | Koa |
|---|---|---|---|---|
| Speed (req/s) | ~45k | ~85k | ~40k | ~50k |
| Bundle size | Minimal | Minimal | Large | Minimal |
| TypeScript | Manual setup | Built-in | Built-in | Manual |
| Middleware | (req,res,next) |
Hooks | Guards/Interceptors | ctx + async |
| Validation | Third-party | JSON Schema | Decorators | Third-party |
| Learning curve | Low | Low | High | Low |
| Ecosystem | Largest | Growing | Enterprise | Minimal |
| Best for | Prototypes, REST APIs | High-performance | Enterprise, NestJS apps | Middleware composition |
Frequently Asked Questions
Q: Should I use Express or Fastify for a new project? Fastify is a strong choice if performance is critical — it's ~2× faster than Express and has built-in JSON Schema validation and TypeScript support. Express is better if you need the widest middleware ecosystem or your team already knows it.
Q: What is the difference between Express middleware and a route handler?
They're the same thing — both are functions of (req, res, next). The difference is intent: middleware transforms/validates the request or adds cross-cutting behaviour; route handlers produce the final response. Route handlers are middleware that typically don't call next().
Q: Is Express single-threaded? Can it handle concurrent requests? Yes, Node.js (and Express) runs on a single thread, but it handles concurrent requests efficiently through the event loop and non-blocking I/O. CPU-intensive work should be offloaded to Worker Threads or a separate service.
Q: How do I use TypeScript with Express?
npm install express && npm install -D typescript @types/express ts-node
import express, { Request, Response, NextFunction } from 'express';
const app = express();
app.get('/users', (req: Request, res: Response) => { res.json([]); });
Q: What's the best way to handle database connections in Express?
Create the connection once at startup (outside the request handler) and reuse it. Use connection pools (e.g., mysql2's createPool, Mongoose's built-in pooling) and close connections on SIGTERM.
Q: Can Express serve a React/Vue SPA?
Yes — build the frontend to a dist/ folder, then:
app.use(express.static('dist'));
app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'dist/index.html')));
The wildcard catch ensures client-side routing works.