Express Framework and MiddlewareIntroductionNode.js provides the runtime for executing JavaScript on the server, but building a complete web application directly with the built-in http module requires developers to manage routing, request parsing, response formatting, errors, and reusable application behavior themselves. Express is a lightweight web framework that organizes these responsibilities into a clear programming model. Express is especially important in full-stack development because it connects browser or mobile clients with application logic, databases, authentication services, and external APIs. Its central ideas are routes and middleware. A route determines how an application responds to a particular HTTP request, while middleware is a function that participates in processing the request and response. This chapter explains how to design Express applications using routing, middleware, controllers, request parsing, response handling, static resources, configuration, and reusable application components. Examples use modern JavaScript and Express 4/5-compatible patterns unless otherwise stated. Learning ObjectivesBy the end of this chapter, you should be able to: - explain the architecture and request lifecycle of an Express application;
- create an Express server and organize it into modules;
- define routes using HTTP methods, parameters, query strings, and routers;
- distinguish application, router, built-in, third-party, and error-handling middleware;
- parse JSON, URL-encoded, and multipart requests appropriately;
- design controllers that separate HTTP concerns from business logic;
- send suitable responses and status codes;
- serve static resources safely;
- manage configuration through environment variables;
- build reusable middleware and application components;
- identify security, reliability, and maintainability risks in Express systems.
1. Express as a Web Application FrameworkAn Express application receives an HTTP request, passes it through an ordered processing pipeline, and eventually sends an HTTP response. The pipeline commonly contains: - configuration and logging;
- security and cross-origin policies;
- request parsing;
- authentication and authorization;
- route matching;
- controller execution;
- error handling.
A simplified conceptual flow is: Client → middleware stack → router → controller/service → response
↘ error handler when a failure occursExpress does not impose a single architecture. This flexibility is useful, but it also means that developers must establish conventions. A small demonstration may place routes and database operations in one file; a professional application normally separates configuration, routes, controllers, services, data access, and middleware. Creating an Express ApplicationInstall Express in a project: npm init -y
npm install express A minimal server is: // app.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Application is running');
});
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});express() creates an application object. The get method registers a handler for HTTP GET requests, and listen binds the application to a TCP port. In production, the port is usually supplied through the environment rather than hard-coded.
2. The Express Request–Response LifecycleThe request object, conventionally named req, contains information supplied by the client and by earlier middleware. The response object, conventionally named res, provides methods for returning data. Important request properties include: | Property | Purpose | Example |
|---|
req.method | HTTP method | GET | req.originalUrl | Original requested URL | /products?limit=10 | req.params | Route parameters | { id: '42' } | req.query | Query-string values | { limit: '10' } | req.body | Parsed request body | { name: 'Keyboard' } | req.headers | Request metadata | Authorization header | req.user | Common custom property | Authenticated user |
Request values are untrusted input. A route parameter or query value should be validated before it is used in a database query, file path, command, or business rule. A response must normally be completed exactly once. Common response methods include: res.send('plain text');
res.json({ message: 'Created' });
res.status(201).json({ id: 7 });
res.redirect('/login');
res.sendStatus(204);res.status(201).json(data) sets the status and serializes an object as JSON. A 204 No Content response must not contain a response body. After sending a response, a handler should return or stop execution to avoid attempting a second response.
3. RoutingRouting maps an HTTP method and URL pattern to one or more handlers. Express supports standard methods such as GET, POST, PUT, PATCH, and DELETE. app.get('/products', listProducts);
app.post('/products', createProduct);
app.patch('/products/:id', updateProduct);
app.delete('/products/:id', deleteProduct);The route /products/:id contains a named parameter. A request to /products/42 produces req.params.id === '42'. Route parameters are strings initially, even when they represent numbers. Query StringsQuery strings commonly express filtering, sorting, or pagination: GET /products?category=books&limit=20 They are available through req.query: app.get('/products', (req, res) => {
const category = req.query.category;
const limit = Number(req.query.limit || 20);
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
return res.status(400).json({ error: 'limit must be an integer from 1 to 100' });
}
res.json({ category, limit });
});A query parameter is not automatically validated or converted. Explicit conversion and boundary checking are therefore essential. Route OrderExpress evaluates matching routes and middleware in registration order. A general route placed before a specific route may intercept requests unexpectedly. Static routes should normally precede parameterized routes when both could match: app.get('/reports/summary', summaryHandler);
app.get('/reports/:id', reportHandler);In this example, /reports/summary is intended as a named endpoint rather than a report whose identifier is summary. Routersexpress.Router() creates a modular routing component:
// routes/productRoutes.js
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
router.get('/', productController.list);
router.get('/:id', productController.getById);
router.post('/', productController.create);
module.exports = router;Mount the router in the application: const productRoutes = require('./routes/productRoutes');
app.use('/api/products', productRoutes);The router's / route is therefore available at /api/products, and its /:id route is available at /api/products/:id. Routers improve cohesion and make versioning possible, for example /api/v1/products and /api/v2/products. 4. MiddlewareMiddleware is a function with access to the request object, response object, and a next function: function exampleMiddleware(req, res, next) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}A middleware function can: - execute code;
- modify
req or res; - terminate the request by sending a response;
- pass control with
next(); - pass an error with
next(error).
If middleware neither sends a response nor calls next, the request remains pending. This is a common source of apparent application hangs. Middleware Categories| Category | Meaning | Typical use |
|---|
| Application-level | Registered on app | Logging, global security | | Router-level | Registered on a router | Product-specific authorization | | Built-in | Supplied by Express | JSON and static-file parsing | | Third-party | Installed separately | CORS, security headers, logging | | Error-handling | Four parameters | Central failure processing | | | |
Application-level middleware runs wherever it is mounted: app.use((req, res, next) => {
req.requestStartedAt = Date.now();
next();
});Router-level middleware is useful when a rule applies only to one resource: router.use(requireStaff);
router.get('/audit', auditController.list);Middleware OrderingOrder is part of application behavior. For example, JSON parsing must occur before a controller that reads req.body, and authentication must occur before authorization. A practical order is: app.use(requestLogger);
app.use(helmet());
app.use(express.json({ limit: '1mb' }));
app.use('/api', apiRoutes);
app.use(notFoundHandler);
app.use(errorHandler);The error handler is placed after routes so it can receive errors generated earlier in the pipeline. Built-in Body ParsingExpress includes parsers for common formats: app.use(express.json());
app.use(express.urlencoded({ extended: false }));express.json() parses requests with an appropriate JSON content type. The parsed value becomes req.body. express.urlencoded() parses HTML form submissions. Body-size limits should be defined to reduce resource-exhaustion risk:
app.use(express.json({ limit: '100kb' }));Multipart forms, especially file uploads, require a specialized library such as Multer. Upload limits, file-type validation, storage location, and malware scanning must be considered; accepting a filename from a client without validation is unsafe. 5. Controllers and Separation of ResponsibilitiesA controller coordinates HTTP input and output. It should not become a large container for database queries, validation rules, authentication policy, and formatting logic. A useful separation is: - Route: identifies the endpoint and middleware;
- Controller: translates HTTP data into an application operation;
- Service: implements business rules;
- Repository or data-access layer: communicates with storage;
- Middleware: handles cross-cutting concerns.
Example controller: // controllers/productController.js
const productService = require('../services/productService');
exports.getById = async (req, res, next) => {
try {
const product = await productService.findById(req.params.id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(product);
} catch (error) {
next(error);
}
};The controller maps absence to 404, while unexpected failures are delegated to centralized error middleware. This produces consistent behavior and makes the service easier to test independently. 6. Validation, Authentication, and AuthorizationValidation confirms that input has the required structure and acceptable values. It should occur near the boundary of the application. A simple manual example is: function validateProduct(req, res, next) {
const { name, price } = req.body;
if (typeof name !== 'string' || name.trim().length < 2) {
return res.status(400).json({ error: 'name is required' });
}
if (typeof price !== 'number' || !Number.isFinite(price) || price < 0) {
return res.status(400).json({ error: 'price must be a non-negative number' });
}
next();
}Production applications often use a schema-validation library, but the principle remains the same: validate type, presence, length, range, and allowed values. Validation is not a substitute for parameterized database queries or output encoding. Authentication establishes who the user is. Authorization determines what that user may do. They are different controls: app.delete('/api/products/:id', authenticate, requireManager, controller.remove);A valid login does not automatically grant permission to delete every product. Middleware should communicate authentication state carefully, and sensitive operations should be protected against session or token misuse. 7. Error HandlingExpress error-handling middleware has four parameters: function errorHandler(err, req, res, next) {
console.error(err);
if (res.headersSent) {
return next(err);
}
const status = err.statusCode || 500;
res.status(status).json({
error: status === 500 ? 'Internal server error' : err.message
});
}The next parameter must be present in the function signature so Express recognizes it as error middleware. Application errors should be represented consistently, while internal details such as stack traces, database credentials, and filesystem paths should not be returned to clients in production. Asynchronous handlers require care. A rejected promise must reach the error handler. In Express versions or project configurations where automatic propagation is unavailable, use a wrapper: const asyncHandler = (handler) => (req, res, next) =>
Promise.resolve(handler(req, res, next)).catch(next); Then: router.get('/:id', asyncHandler(productController.getById));A not-found handler is distinct from an error handler: function notFoundHandler(req, res) {
res.status(404).json({ error: 'Endpoint not found' });
}8. Static ResourcesExpress can serve files such as stylesheets, browser JavaScript, and images: const path = require('path');
app.use(express.static(path.join(__dirname, 'public')));A file public/css/site.css is then available at /css/site.css. A virtual prefix can clarify the public URL: app.use('/assets', express.static(path.join(__dirname, 'public')));Static serving should use an explicit directory, not a path derived directly from user input. Do not place secrets, source-control metadata, environment files, private uploads, or database backups inside a public directory. Cache headers, compression, content types, and a content delivery network may be considered for production deployments. 9. Configuration and Environment ManagementConfiguration includes port numbers, database URLs, token settings, allowed origins, logging levels, and feature flags. Environment variables keep deployment-specific values outside source code: const config = {
port: Number(process.env.PORT || 3000),
nodeEnv: process.env.NODE_ENV || 'development',
databaseUrl: process.env.DATABASE_URL,
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:5173'
};
if (!config.databaseUrl && config.nodeEnv === 'production') {
throw new Error('DATABASE_URL is required in production');
}Configuration should be loaded once, validated at startup, and passed to components explicitly where practical. Secrets should not be committed to version control or printed in logs. A .env file may be convenient for local development, but production secret management should use an appropriate deployment or cloud secret system. 10. Reusable Application ComponentsReusable components reduce duplication and make behavior consistent. Examples include: - request logging middleware;
- correlation or request IDs;
- authentication and role checks;
- validation middleware;
- pagination helpers;
- rate-limiting middleware;
- standardized response and error classes;
- health and readiness endpoints.
A request-ID middleware illustrates a small reusable component: const crypto = require('crypto');
function requestId(req, res, next) {
const id = req.get('x-request-id') || crypto.randomUUID();
req.requestId = id;
res.set('x-request-id', id);
next();
}In a distributed system, this identifier can connect application logs, database traces, and downstream requests. If a client supplies an identifier, the application should apply an acceptable format and length policy rather than trusting arbitrary values. 11. Applied Case: Inventory API for a Campus StoreBackgroundA campus store requires an API for listing products, viewing a product, and allowing authorized staff to create products. The API must support JSON clients and return predictable errors. ProblemAn initial single-file implementation performs no validation, returns database errors directly, and allows any authenticated user to create products. It is difficult to test and risks exposing internal information. ApproachThe application is reorganized into routes, middleware, controllers, and a service layer. // app.js
const express = require('express');
const productRoutes = require('./routes/productRoutes');
const { notFoundHandler, errorHandler } = require('./middleware/errors');
const app = express();
app.use(express.json({ limit: '100kb' }));
app.use('/api/products', productRoutes);
app.use(notFoundHandler);
app.use(errorHandler);
module.exports = app;// routes/productRoutes.js
const express = require('express');
const router = express.Router();
const controller = require('../controllers/productController');
const validateProduct = require('../middleware/validateProduct');
const authenticate = require('../middleware/authenticate');
const requireStaff = require('../middleware/requireStaff');
router.get('/', controller.list);
router.get('/:id', controller.getById);
router.post('/', authenticate, requireStaff, validateProduct, controller.create);
module.exports = router;The public GET routes are separated from the protected POST route. Validation runs after authentication but before the controller. The controller can then assume that the basic request shape has been checked, while the service remains responsible for business rules such as unique product codes and stock constraints. OutcomeThe resulting design has clearer responsibilities, consistent HTTP behavior, safer error exposure, and smaller units for testing. A later requirement—such as adding an administrator-only delete route—can be implemented by composing existing authentication and authorization middleware rather than copying code. Learning Points- Route design expresses the public API contract.
- Middleware order affects correctness and security.
- Controllers should coordinate rather than contain every rule.
- Error handling is part of the API design, not an afterthought.
- Modular structure supports testing and future change.
12. Security, Reliability, and Professional PracticeExpress applications should be developed using layered controls: - use HTTPS through the deployment platform or reverse proxy;
- set security-related headers, commonly with a maintained security middleware;
- restrict CORS to trusted origins where cross-origin access is needed;
- apply rate limits to login, password reset, and expensive endpoints;
- validate request bodies, parameters, headers, and uploaded files;
- use parameterized database operations;
- avoid returning stack traces and secrets;
- define body-size, timeout, and upload limits;
- log security-relevant events without logging passwords or tokens;
- keep Express and dependencies patched;
- implement graceful shutdown and health checks.
CORS is a browser policy, not an authentication mechanism. Similarly, middleware that checks a token does not by itself guarantee that an object belongs to the requesting user. Object-level authorization must be enforced in the service or data-access operation. Privacy and governance also matter. Logs may contain personal data, IP addresses, or identifiers. Organizations should establish retention, access, masking, and incident-response policies. API documentation should describe authentication, status codes, validation failures, pagination, and versioning so that clients can use the service reliably. 13. Testing and MaintainabilityA maintainable Express application is tested at several levels: - Unit tests: test validation, services, and pure utility functions in isolation.
- Integration tests: test routes with middleware and a controlled database or test double.
- End-to-end tests: test realistic workflows across deployed components.
Tests should verify successful responses and failure conditions: malformed JSON, missing authentication, forbidden roles, nonexistent resources, duplicate records, and unexpected service failures. The application should export app separately from the file that calls listen, making it easier for a test runner to create requests without opening a production port. Documentation tools such as OpenAPI can describe route parameters, request bodies, authentication, and response schemas. Consistent naming and a predictable error format reduce the cost of client integration. Contemporary DevelopmentsModern Express projects commonly use TypeScript for static type checking, schema libraries for runtime validation, structured logging, containerized deployment, and observability platforms. Express can also be placed behind a reverse proxy or API gateway that terminates TLS, applies rate limits, and routes traffic. These tools complement rather than replace sound route design and middleware discipline. AI-assisted coding can generate route and middleware examples, but generated code must be reviewed for authorization flaws, unsafe file handling, dependency risks, and incorrect assumptions about Express versions. Security and correctness remain the developer's responsibility. Chapter SummaryExpress provides a focused framework for constructing Node.js web applications. Its request–response model is organized around ordered middleware and route handlers. Routes use HTTP methods, parameters, and query strings to expose application capabilities, while routers group related endpoints into reusable modules. Middleware can inspect requests, enrich context, enforce policies, parse bodies, serve static resources, or handle failures. Correct ordering is essential: parsers must precede body-dependent handlers, authentication must precede authorization, and error middleware must be registered after normal routes. Controllers should translate HTTP operations into service calls rather than becoming large, tightly coupled application components. Professional Express applications validate untrusted input, return meaningful status codes, centralize error handling, protect static resources, manage configuration through validated environment settings, and apply security controls such as authentication, authorization, rate limiting, secure headers, and safe logging. Modular design and testing make the application easier to extend and operate. Key TermsExpress: A Node.js web framework for HTTP applications. Route: A method and path pattern mapped to one or more handlers. Middleware: A function that participates in request and response processing. Application middleware: Middleware registered on the main Express application. Router: A modular Express object containing related routes and middleware. Controller: A component that translates HTTP requests into application operations. Service layer: A component containing business operations and rules. Request parameter: A named variable embedded in a route path. Query string: Optional key-value data appended to a URL. Request body: Data sent by a client, commonly as JSON or form data. Status code: A numeric HTTP result classification such as 200, 404, or 500. Validation: Checking input structure, type, and allowed values. Authentication: Establishing the identity of a client or user. Authorization: Determining whether an identified user may perform an action. Error-handling middleware: Middleware with the signature (err, req, res, next). Static resource: A file served without generating it dynamically for each request. Environment variable: Deployment-specific configuration supplied outside source code. CORS: Browser-enforced rules controlling cross-origin requests. Request ID: An identifier used to correlate logs and operations for one request. Review QuestionsShort Answer Questions- What is the purpose of
next() in Express middleware? - How do route parameters differ from query-string values?
- Why should error-handling middleware be registered after application routes?
- What is the difference between authentication and authorization?
- Why should request bodies and route parameters be validated?
Long Answer Questions- Explain the Express request–response lifecycle and describe how middleware ordering affects it.
- Compare application-level middleware, router-level middleware, built-in middleware, and error-handling middleware.
- Design a modular Express structure for a product API and explain the responsibilities of its routes, controllers, services, and repositories.
- Explain how an Express application should handle configuration, secrets, static files, and production error responses.
- Discuss the security and maintainability benefits of validation, centralized error handling, authentication, authorization, and automated testing.
Analytical / Application Questions- A route reads
req.body, but every request produces undefined. Analyze the likely middleware-order problem and propose a correction. - An authenticated user can request
/api/orders/81 and view another user's order. Identify the security weakness and design a suitable authorization check. - A campus store API has public product listing, staff-only creation, and administrator-only deletion. Design the route and middleware sequence for these three operations, explaining why each component is placed where it is.
Multiple Choice Questions with AnswersQuestion 1Which Express object is used to group related routes and middleware? A. express.Router() B. express.Response() C. express.Database() D. express.Config() Correct Answer: A. express.Router() Question 2Where are named route parameters such as the id in /products/:id available? A. req.query B. req.params C. req.headers D. req.body Correct Answer: B. req.params Question 3What must middleware do when it neither sends a response nor handles an error? A. Call res.end() automatically B. Restart the server C. Call next() D. Modify the HTTP method Correct Answer: C. Call next() Question 4Which built-in middleware parses JSON request bodies? A. express.static() B. express.json() C. express.router() D. express.errors() Correct Answer: B. express.json() Question 5Which status code is most appropriate when a requested resource does not exist? A. 201 B. 204 C. 301 D. 404 Correct Answer: D. 404 Question 6What distinguishes Express error-handling middleware? A. It has four parameters: err, req, res, and next B. It must always be registered first C. It can only process GET requests D. It cannot send a response Correct Answer: A. It has four parameters: err, req, res, and next Question 7What is the main purpose of express.static()? A. To authenticate users B. To serve files such as images and stylesheets C. To parse SQL queries D. To create database tables Correct Answer: B. To serve files such as images and stylesheets Question 8Which practice best protects deployment-specific secrets? A. Hard-code them in route files B. Place them in a public directory C. Use environment or managed secret configuration D. Return them in error responses Correct Answer: C. Use environment or managed secret configuration Question 9Why should a controller usually delegate business rules to a service layer? A. To make HTTP methods unnecessary B. To separate HTTP concerns from reusable application logic C. To prevent all validation D. To ensure every route returns HTML Correct Answer: B. To separate HTTP concerns from reusable application logic Question 10A client sends limit=20 in a query string. What type does Express normally provide for this value before explicit conversion? A. Boolean B. Number C. String D. Date Correct Answer: C. String
|