1. What is Express.js and why is it used?
Express.js is a minimal, unopinionated web framework for Node.js that simplifies building web servers and APIs. It wraps Node's built-in http module with a clean routing system, a middleware pipeline and helpers for requests and responses. Developers use it because it is fast to set up, has a huge ecosystem of middleware, and leaves architectural choices to you instead of forcing a structure.
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello'));
app.listen(3000);
2. What is middleware in Express?
Middleware is a function that has access to the request object, the response object and the next function, and runs in the request-response cycle. It can run code, modify req and res, end the response, or call next() to pass control onward. Examples include logging, authentication, body parsing and error handling. Middleware executes in the order it is registered.
app.use((req, res, next) => {
console.log(req.method, req.url);
next();
});
3. What is the role of the `next()` function?
next() passes control to the next matching middleware or route handler in the stack. If a middleware neither sends a response nor calls next(), the request hangs. Calling next(err) with an argument skips the remaining regular handlers and jumps straight to error-handling middleware. Calling next('route') skips the rest of the current route's handlers.
4. How does routing work in Express?
Routing maps an HTTP method and URL path to one or more handler functions. You define routes with methods such as app.get(), app.post(), app.put() and app.delete(), and Express matches incoming requests in the order the routes were registered. The first matching handler that sends a response ends the cycle. app.all() matches every method for a path.
app.get('/users', listUsers);
app.post('/users', createUser);
5. What is the difference between `app.use()` and `app.get()`?
app.use() mounts middleware for every HTTP method and matches any path that starts with the given prefix, while app.get() handles only GET requests and matches the path exactly. app.use() is meant for middleware such as logging or parsers, and app.get() is for defining endpoints. Also, inside app.use('/api', fn), req.url has the mount path stripped.
6. What are the `req` and `res` objects?
req represents the incoming HTTP request and res represents the response Express will send back. req exposes data such as req.params, req.query, req.body, req.headers and req.cookies (with a parser). res provides methods such as res.send(), res.json(), res.status(), res.redirect() and res.sendFile(). Both extend Node's native IncomingMessage and ServerResponse.
7. What is the difference between `req.params`, `req.query` and `req.body`?
req.params holds values from route path parameters, req.query holds parsed query string values, and req.body holds the parsed request payload. For /users/:id?sort=asc, req.params.id is the id and req.query.sort is asc. req.body is undefined until a body-parsing middleware such as express.json() is applied.
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, sort: req.query.sort });
});
8. How do you parse JSON and form data in Express?
Use the built-in express.json() for JSON bodies and express.urlencoded() for HTML form submissions. Since Express 4.16 these are included, so body-parser is no longer required as a separate install. Register them with app.use() before your routes, otherwise req.body will be undefined. For multipart file uploads you need a library such as multer.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
9. What is `express.Router` and why use it?
express.Router creates a modular, mountable mini-application that can hold its own routes and middleware. It lets you split a large app into separate files per feature, such as users or orders, and mount each under a path prefix. This keeps app.js small and makes routes easier to test and maintain.
const router = express.Router();
router.get('/', listUsers);
router.get('/:id', getUser);
app.use('/users', router);
10. How do you serve static files in Express?
Use the built-in express.static() middleware and pass it the directory that contains your files. Files such as images, CSS and client-side JavaScript are then served directly from that folder. You can add a mount prefix as the first argument, and options such as maxAge for caching.
app.use(express.static('public'));
app.use('/assets', express.static('public', { maxAge: '1d' }));
11. How does error handling work in Express?
Express has a special error-handling middleware with four arguments: (err, req, res, next). It must be registered after all other routes and middleware. When a handler throws synchronously or calls next(err), Express skips normal handlers and calls the error middleware. Express also ships a default error handler that returns a stack trace outside production.
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({ message: err.message });
});
12. How do you handle errors in async route handlers?
In Express 4, errors thrown inside async handlers are not caught automatically, so you must wrap the code in try/catch and call next(err), or use a wrapper utility. Express 5 fixes this: rejected promises returned from handlers and middleware are automatically forwarded to the error handler. Without either approach, an unhandled rejection can crash the process or leave the request hanging.
const wrap = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get('/u', wrap(async (req, res) => res.json(await getUsers())));
13. What is the difference between `res.send()`, `res.json()` and `res.end()`?
res.send() sends a response of various types and sets Content-Type automatically, res.json() serialises a value as JSON and sets the JSON content type, and res.end() is Node's low-level method that ends the response without setting any headers or type. Use res.json() for APIs, res.send() for text or HTML, and res.end() rarely, for empty responses.
14. How do you enable CORS in Express?
Install the cors package and register it as middleware, or set the Access-Control-Allow-* headers yourself. By default cors() allows all origins, so in production you should restrict it to known origins. The middleware also answers preflight OPTIONS requests for you.
const cors = require('cors');
app.use(cors({
origin: 'https://example.com',
methods: ['GET', 'POST'],
credentials: true
}));
15. What is Helmet and how does it improve security?
Helmet is a middleware that sets various secure HTTP response headers to reduce common web vulnerabilities. It sets headers such as Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options and removes X-Powered-By. It is not a complete security solution, but it is a cheap and standard first layer of protection.
const helmet = require('helmet');
app.use(helmet());
16. What are the best practices for securing an Express application?
Use helmet for secure headers, restrict CORS to trusted origins, validate and sanitise all input, and apply rate limiting with express-rate-limit. Always serve over HTTPS, keep dependencies updated with npm audit, and never leak stack traces in production. Store secrets in environment variables, use parameterised queries to prevent injection, and set cookies as httpOnly, secure and sameSite.
17. What are cookies and how do you work with them in Express?
Cookies are small pieces of data stored in the browser and sent back with every request to the same site. You set them with res.cookie() and read them from req.cookies after adding cookie-parser. For sensitive values, set httpOnly, secure and sameSite options, and use signed cookies if you need tamper detection.
app.use(require('cookie-parser')('secret'));
res.cookie('token', value, { httpOnly: true, secure: true, sameSite: 'strict' });
18. How do sessions work in Express?
Sessions store per-user state on the server and identify the user with a session ID kept in a cookie. With express-session, the data lives in req.session, and the browser only holds the ID. The default memory store is not suitable for production because it leaks memory and does not scale across processes, so use a store such as Redis.
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true }
}));
19. What template engines can you use with Express?
Express supports any template engine that provides an __express function or is wrapped by consolidate, including Pug, EJS and Handlebars. You set the folder with the views setting and the engine with the view engine setting, then render with res.render(). Templates produce HTML on the server by merging data into a view.
app.set('view engine', 'ejs');
app.set('views', './views');
app.get('/', (req, res) => res.render('index', { title: 'Home' }));
20. How do you structure a large Express application?
Separate concerns into layers: routes define endpoints, controllers handle request and response logic, services hold business logic, and models talk to the database. Keep configuration, middleware and utilities in their own folders, and split app.js (app setup) from server.js (listening) so the app can be tested without opening a port. Organising by feature also works well in larger codebases.
src/
routes/
controllers/
services/
models/
middleware/
app.js
server.js
21. What is the difference between application-level and router-level middleware?
Application-level middleware is bound to the app instance with app.use() or app.METHOD(), and applies to the whole application. Router-level middleware works the same way but is bound to an express.Router instance, so it only applies to routes mounted on that router. Router-level middleware is useful for protecting a group of routes, such as an admin section.
const admin = express.Router();
admin.use(requireAdmin);
app.use('/admin', admin);
22. What is the difference between `res.redirect()` and `res.render()`?
res.render() renders a view template with data and sends the resulting HTML as the response, while res.redirect() sends a 3xx status and a Location header so the browser makes a new request to another URL. Use redirect after a successful form POST to avoid duplicate submissions. The default redirect status is 302, and you can pass 301 or 303 explicitly.
23. How do you handle 404 errors in Express?
Add a catch-all middleware after all your routes that sends a 404 response. Because Express evaluates handlers in order, anything that reaches this middleware did not match an earlier route. You can also create an error with status 404 and pass it to next() so a central error handler formats the response.
app.use((req, res) => {
res.status(404).json({ message: 'Not found' });
});
24. How do you use environment variables and configuration in Express?
Read them through process.env, and load a .env file during development with the dotenv package (or Node's --env-file flag). Keep secrets such as database URLs and API keys out of source control. Express also has app.set() and app.get() for settings, and uses NODE_ENV to switch behaviour, for example enabling view caching and hiding error details when it is production.
require('dotenv').config();
const port = process.env.PORT || 3000;
app.listen(port);
25. How do you define route parameters and optional or validated parameters?
Define named parameters with a colon, such as /users/:id, and read them from req.params. You can pre-process a parameter once with router.param(). Optional and regex syntax changed in Express 5, which uses newer path-to-regexp rules: optional segments use braces like /users{/:id}, and inline regex in the path is no longer supported, so validate in a handler or with a validation library.
app.param('id', (req, res, next, id) => {
if (!/^\d+$/.test(id)) return res.status(400).send('Bad id');
next();
});
app.get('/users/:id', (req, res) => res.send(req.params.id));
26. Why does the order of middleware registration matter in Express?
Express runs middleware and routes strictly in the order they are registered, so a handler registered too late may never run. For example, a body parser placed after a route leaves req.body undefined, and a 404 catch-all placed before routes swallows every request. Authentication, parsers and logging should come first, routes next, then the 404 handler and finally the error handler.
app.get('/a', (req, res) => res.json(req.body));
app.use(express.json());
27. What is wrong with calling `res.send()` twice in a handler?
The second call throws the error "Cannot set headers after they are sent to the client" (ERR_HTTP_HEADERS_SENT), because the response has already started. It usually happens when a branch forgets to return after sending a response, or when code continues after next(). Always return res.json(...) in early exits so execution stops there.
app.get('/u', (req, res) => {
if (!req.query.id) res.status(400).send('Missing id');
res.send('ok');
});
28. How do you create a custom error class and centralise error handling?
Define an error class that carries an HTTP status, throw it from anywhere, and let a single error-handling middleware translate it into a response. This removes repeated res.status().json() code from handlers and keeps error formats consistent. Unknown errors fall back to a generic 500 so internal details are never leaked.
class HttpError extends Error {
constructor(status, message) { super(message); this.status = status; }
}
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({ error: status < 500 ? err.message : 'Server error' });
});
29. How do you validate request input in Express?
Validate the body, query and params at the edge using a schema library such as zod, joi or express-validator, and reject bad input with a 400 or 422 before it reaches business logic. Wrapping validation in reusable middleware keeps handlers clean. Never trust client data, and prefer allow-listing known fields over stripping bad ones.
const validate = schema => (req, res, next) => {
const r = schema.safeParse(req.body);
if (!r.success) return res.status(400).json(r.error.issues);
req.body = r.data;
next();
};
30. How would you implement JWT authentication in Express?
Verify credentials on login, sign a short-lived token with jsonwebtoken, and add a middleware that reads the Authorization: Bearer header, verifies the signature and attaches the user to req. Keep the secret in an environment variable, set an expiry, and use refresh tokens for longer sessions. Remember that a JWT cannot be revoked easily, so keep lifetimes short.
function auth(req, res, next) {
const token = (req.headers.authorization || '').split(' ')[1];
try { req.user = jwt.verify(token, process.env.JWT_SECRET); next(); }
catch { res.status(401).json({ error: 'Unauthorized' }); }
}
31. What is the difference between authentication and authorization, and how do you implement role checks?
Authentication proves who the user is, while authorization decides what that user may do. In Express, authentication middleware runs first and sets req.user, then an authorization middleware checks the role or permission and returns 403 if it is not allowed. Return 401 for missing or invalid credentials and 403 for a valid user without permission.
const requireRole = role => (req, res, next) =>
req.user?.role === role ? next() : res.sendStatus(403);
app.delete('/users/:id', auth, requireRole('admin'), removeUser);
32. How do you implement rate limiting in Express and what are its limits?
Use express-rate-limit to cap the number of requests per client within a time window, and apply stricter limits to sensitive routes such as login. The default store is in memory, so with several processes or servers you need a shared store such as Redis, otherwise each instance counts separately. Behind a proxy you must also configure trust proxy, or every client appears to share one IP.
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 100 });
app.use('/api', limiter);
33. What does `app.set('trust proxy', true)` do and when do you need it?
It tells Express to trust the X-Forwarded-* headers set by a reverse proxy or load balancer, so req.ip, req.protocol and req.secure reflect the real client instead of the proxy. You need it behind Nginx, AWS ELB or Heroku, for example, for rate limiting and secure cookies to work. Prefer a specific hop count or subnet over a blanket true, because trusting everything lets clients spoof their IP.
app.set('trust proxy', 1);
34. How do you handle file uploads in Express?
Use multer for multipart/form-data, because express.json() and express.urlencoded() do not parse files. Configure storage, limit the file size and filter by MIME type or extension, and never trust the original file name. For large files or production, stream directly to object storage such as S3 instead of keeping them on local disk or in memory.
const upload = multer({ dest: 'uploads/', limits: { fileSize: 5 * 1024 * 1024 } });
app.post('/avatar', upload.single('avatar'), (req, res) => {
res.json({ file: req.file.filename });
});
35. What happens if you do heavy synchronous work in a route handler?
It blocks Node's single-threaded event loop, so every other request waits until the work finishes and the whole server appears frozen. Examples are JSON.parse on huge payloads, synchronous crypto, fs.readFileSync and big loops. Move CPU-heavy work to worker_threads, a child process or a job queue, and always use asynchronous APIs in handlers.
app.get('/hash', (req, res) => {
const h = crypto.pbkdf2Sync(pw, salt, 1e6, 64, 'sha512');
res.send(h.toString('hex'));
});
36. How can you scale an Express application across multiple CPU cores?
Run several processes, because one Node process uses only one core. You can use the built-in cluster module, a process manager such as PM2 in cluster mode, or run multiple containers behind a load balancer. Since the processes do not share memory, move sessions, caches and rate-limit counters into an external store such as Redis, and keep the app stateless.
pm2 start server.js -i max
37. How do you implement graceful shutdown in an Express server?
Listen for SIGTERM and SIGINT, stop accepting new connections with server.close(), wait for in-flight requests to finish, close database connections, and then exit. Add a timeout that forces exit if something hangs. This prevents dropped requests during deployments and container restarts.
const server = app.listen(3000);
process.on('SIGTERM', () => {
server.close(async () => { await db.close(); process.exit(0); });
setTimeout(() => process.exit(1), 10000).unref();
});
38. How do you enable response compression and caching in Express?
Add the compression middleware to gzip responses, and ideally let a reverse proxy or CDN do it in production. For caching, set Cache-Control headers explicitly, use maxAge in express.static, and rely on the built-in ETag support, which lets browsers revalidate with If-None-Match and receive a 304. Do not cache personalised or authenticated responses publicly.
app.use(compression());
app.use(express.static('public', { maxAge: '7d', etag: true }));
39. What are the main differences between Express 4 and Express 5?
Express 5 forwards rejected promises from async handlers to the error handler automatically, and it removes deprecated methods such as app.del() and res.sendfile(). It also uses a newer path-to-regexp, so wildcards must be named like /*splat, and req.body is undefined instead of {} when no parser ran. Express 5 requires a recent Node.js version, so check the migration guide before upgrading.
app.get('/files/*splat', (req, res) => res.send(req.params.splat));
40. How do you test an Express application?
Export the app without calling listen(), then use supertest with a test runner such as Jest or Vitest to send real HTTP requests against it in memory. Test status codes, bodies and headers, and mock or use a test database for external dependencies. Separating app.js from server.js is what makes this easy.
const request = require('supertest');
test('GET /health', async () => {
const res = await request(app).get('/health');
expect(res.status).toBe(200);
});
41. How do you implement API versioning in Express?
The usual approach is to mount separate routers under a version prefix such as /api/v1 and /api/v2, which is simple, cacheable and easy to route. Alternatives are a custom header or an Accept media type, which keep URLs clean but are harder to test and cache. Share services between versions and only duplicate the parts that actually changed.
app.use('/api/v1', require('./routes/v1'));
app.use('/api/v2', require('./routes/v2'));
42. How do you handle CORS preflight requests, and why do they happen?
Browsers send an OPTIONS preflight request before a cross-origin request that is not "simple", for example one with a JSON content type, custom headers or a method such as PUT or DELETE. The server must reply with the matching Access-Control-Allow-* headers. The cors middleware handles this automatically, but it must be registered before your routes and auth middleware, otherwise the preflight is rejected with 401.
app.use(cors({ origin: 'https://app.example.com', credentials: true }));
app.use(auth);
43. How do you log requests in Express, and what should you avoid logging?
Use morgan for simple HTTP access logs or a structured logger such as pino or winston with a request ID so one request can be traced across log lines. Write logs as JSON in production and ship them to a central system. Never log passwords, tokens, full card numbers or whole request bodies, as these end up in plain text in log storage.
const morgan = require('morgan');
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
44. How do you prevent NoSQL and SQL injection in an Express app?
Never build queries by concatenating user input. For SQL, use parameterised queries or an ORM, and for MongoDB, validate that fields are the expected primitive types, because a JSON body like { "password": { "$ne": null } } can bypass a login check. Schema validation, allow-listing fields and least-privilege database accounts add further protection.
const email = String(req.body.email);
const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
45. What is the difference between `res.sendFile()` and `res.download()`, and what security risk should you watch for?
res.sendFile() sends a file for the browser to display, while res.download() sets Content-Disposition: attachment so the browser saves it. Both need an absolute path or a root option. The main risk is path traversal: if you pass user input into the path, a value like ../../etc/passwd can expose server files, so use the root option and validate the name.
app.get('/files/:name', (req, res) => {
res.download(req.params.name, { root: '/srv/files' });
});
46. How do you stream large responses in Express instead of buffering them?
Pipe a readable stream into res, since res is a writable stream. This keeps memory use low and starts sending data immediately, which is ideal for large files, exports and proxied content. Handle the stream's error event, otherwise a failure mid-transfer leaves the client hanging, and use stream.pipeline for proper cleanup.
const { pipeline } = require('stream');
app.get('/export', (req, res) => {
res.type('text/csv');
pipeline(fs.createReadStream('big.csv'), res, err => err && console.error(err));
});
47. What is the difference between `app.locals`, `res.locals` and `req` properties for sharing data?
app.locals lives for the whole application lifetime and holds app-wide values such as a site name. res.locals lives for a single request and is also exposed to templates rendered with res.render(). Attaching data to req, such as req.user, is the common way to pass values between middleware within one request. Never store per-user data in app.locals, because it is shared by all requests.
app.use((req, res, next) => {
res.locals.user = req.user;
next();
});
48. How do you handle long-running tasks or background jobs in an Express API?
Do not do the work inside the request, because it ties up connections and times out. Instead, enqueue a job in a queue such as BullMQ or SQS, return 202 Accepted with a job ID, and let a separate worker process it. The client can then poll a status endpoint or receive a webhook or WebSocket notification when the job completes.
app.post('/reports', async (req, res) => {
const job = await queue.add('report', req.body);
res.status(202).json({ jobId: job.id, status: `/reports/${job.id}` });
});
49. How do you handle request timeouts and protect against slow clients?
Set timeouts on the underlying Node server, such as server.requestTimeout, headersTimeout and keepAliveTimeout, to drop slow or stalled connections (slowloris attacks). Also limit body sizes with express.json({ limit }) and set timeouts on outgoing database and HTTP calls. A per-route timeout middleware can reply with 503, but it does not cancel the work already running, so use AbortController where possible.
app.use(express.json({ limit: '100kb' }));
server.requestTimeout = 30000;
server.keepAliveTimeout = 65000;
50. What are common Express performance and production best practices?
Set NODE_ENV=production, use asynchronous code everywhere, enable compression, cache static assets and use a reverse proxy such as Nginx for TLS and static files. Use connection pooling for databases, structured logging instead of console.log, health check endpoints, and a process manager or orchestrator for restarts. Avoid synchronous calls, large payloads and memory leaks from global state or the default session store.
app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));