Backend Development with Node.js/Express

Chapter 6
Express Framework and Middleware

Diploma in Full Stack Programming
Backend Development with Node.js/Express   •   Diploma

Express Framework and Middleware

Introduction

Node.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 Objectives

By 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 Framework

An Express application receives an HTTP request, passes it through an ordered processing pipeline, and eventually sends an HTTP response. The pipeline commonly contains:

  1. configuration and logging;
  2. security and cross-origin policies;
  3. request parsing;
  4. authentication and authorization;
  5. route matching;
  6. controller execution;
  7. error handling.

A simplified conceptual flow is:

Client → middleware stack → router → controller/service → response
                         ↘ error handler when a failure occurs

Express 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 Application

Install 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 Lifecycle

The 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:

PropertyPurposeExample
req.methodHTTP methodGET
req.originalUrlOriginal requested URL/products?limit=10
req.paramsRoute parameters{ id: '42' }
req.queryQuery-string values{ limit: '10' }
req.bodyParsed request body{ name: 'Keyboard' }
req.headersRequest metadataAuthorization header
req.userCommon custom propertyAuthenticated 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. Routing

Routing 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 Strings

Query 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 Order

Express 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.

Routers

express.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. Middleware

Middleware 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

CategoryMeaningTypical use
Application-levelRegistered on appLogging, global security
Router-levelRegistered on a routerProduct-specific authorization
Built-inSupplied by ExpressJSON and static-file parsing
Third-partyInstalled separatelyCORS, security headers, logging
Error-handlingFour parametersCentral 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 Ordering

Order 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 Parsing

Express 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 Responsibilities

A 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 Authorization

Validation 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 Handling

Express 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 Resources

Express 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 Management

Configuration 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 Components

Reusable 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 Store

Background

A 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.

Problem

An 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.

Approach

The 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.

Outcome

The 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

  1. Route design expresses the public API contract.
  2. Middleware order affects correctness and security.
  3. Controllers should coordinate rather than contain every rule.
  4. Error handling is part of the API design, not an afterthought.
  5. Modular structure supports testing and future change.

12. Security, Reliability, and Professional Practice

Express 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 Maintainability

A 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 Developments

Modern 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 Summary

Express 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 Terms

Express: 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 Questions

Short Answer Questions

  1. What is the purpose of next() in Express middleware?
  2. How do route parameters differ from query-string values?
  3. Why should error-handling middleware be registered after application routes?
  4. What is the difference between authentication and authorization?
  5. Why should request bodies and route parameters be validated?

Long Answer Questions

  1. Explain the Express request–response lifecycle and describe how middleware ordering affects it.
  2. Compare application-level middleware, router-level middleware, built-in middleware, and error-handling middleware.
  3. Design a modular Express structure for a product API and explain the responsibilities of its routes, controllers, services, and repositories.
  4. Explain how an Express application should handle configuration, secrets, static files, and production error responses.
  5. Discuss the security and maintainability benefits of validation, centralized error handling, authentication, authorization, and automated testing.

Analytical / Application Questions

  1. A route reads req.body, but every request produces undefined. Analyze the likely middleware-order problem and propose a correction.
  2. An authenticated user can request /api/orders/81 and view another user's order. Identify the security weakness and design a suitable authorization check.
  3. 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 Answers

Question 1

Which 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 2

Where 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 3

What 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 4

Which 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 5

Which 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 6

What 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 7

What 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 8

Which 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 9

Why 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 10

A 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

Backend Development with Node.js/Express
Chapter 6 of 15