Advanced Web Programming

HTTP Servers, Express, Routing and Middleware

PGCP-AC

An HTTP server turns protocol messages into application operations and application results back into protocol responses. Express organizes this work as an ordered pipeline of middleware and route handlers. Correct order, validation, error flow and response completion determine whether the service is dependable.

1. Handling a request

Node's HTTP module exposes server creation and request/response objects. Express builds routing and middleware conventions on top of Node. A route combines an HTTP method with a path pattern. Route parameters identify path components, query parameters carry URL options and the body carries a submitted representation. These inputs remain untrusted even when a parser has successfully decoded them.

Middleware executes in registration order. It can inspect or modify the request, send a response or pass control with next(). Failing both to finish the response and to continue leaves a request waiting. A JSON parser must run before handlers that expect parsed JSON bodies. Error-handling middleware has four parameters: error, request, response and next. A response should normally be completed once; sending again can produce a headers-already-sent error.

2. Layering and API design

Separate request handling, business logic and persistence. A controller translates HTTP input; a service enforces operations and rules; a data-access layer communicates with storage. Templates combine data with a view to produce HTML. Static-file middleware serves assets without requiring a custom route for each file. REST-oriented APIs identify resources and use method semantics and meaningful status codes.

app.use(express.json());
app.get('/items/:id', (req, res) => {
  const item = items.find(x => x.id === Number(req.params.id));
  if (!item) return res.status(404).json({ error: 'Not found' });
  res.json(item);
});

The early return prevents a second response after the error. Real applications also validate identifiers, handle persistence failures and enforce authorization. Authentication establishes identity; authorization decides which operations that identity may perform. Neither belongs solely in a visual button-hiding rule.

3. HTTP without a framework

import http from 'node:http';
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Service is running');
});
server.listen(3000);

The response is completed with end; a route that merely calculates a result without ending the response leaves the client waiting. For a submitted request body, Node exposes incoming data as a stream. Express body parsers organize this work for supported formats, but limits and error handling still matter.

A template route might call res.render('profile', { user }). The template engine then controls escaping and layout. Route-specific middleware can check a role before the handler runs; application-wide middleware can log request metadata or establish common context. Place a not-found handler after intended routes so it does not intercept every request prematurely.

4. Node HTTP request and response objects

The request object is a readable stream containing method, target, headers and optional body bytes. The response object is a writable stream used to set status, headers and content.

import http from 'node:http';

const server = http.createServer((request, response) => {
  if (request.method === 'GET' && request.url === '/health') {
    response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
    response.end(JSON.stringify({ status: 'ok' }));
    return;
  }
  response.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
  response.end(JSON.stringify({ error: 'Not found' }));
});

server.listen(3000);

end() completes the response. A handler that neither ends the response nor deliberately keeps it streaming leaves the client waiting. Headers must be decided before body bytes commit them.

Collecting an incoming body requires limits and error handling. An unlimited body allows memory exhaustion; invalid JSON is a client error rather than a reason to crash the process.

5. Express application and route matching

Express wraps Node request and response objects with routing and convenience methods.

import express from 'express';
const app = express();

app.use(express.json({ limit: '100kb' }));

app.get('/courses/:courseId/topics/:topicId', (req, res) => {
  const { courseId, topicId } = req.params;
  const includeTest = req.query.includeTest === 'true';
  res.json({ courseId, topicId, includeTest });
});

The route method and path both participate in matching. Path parameters identify parts of a resource path. Query parameters modify retrieval, filtering, sorting, pagination or representation. The body carries a submitted representation. Headers carry metadata and protocol controls. All are untrusted strings or decoded values until validated.

Specific and generic routes can conflict. Register intended routes deliberately and test ambiguous patterns rather than assuming the router will infer priority from business meaning.

6. Middleware pipeline

Middleware runs in registration order when its path and conditions match. It can:

  • inspect or add request context;
  • change response headers;
  • complete the response;
  • call next() to continue;
  • report failure through the framework's error path.
app.use((req, res, next) => {
  req.requestStartedAt = Date.now();
  res.on('finish', () => {
    console.log(req.method, req.originalUrl, res.statusCode,
      Date.now() - req.requestStartedAt);
  });
  next();
});

If middleware sends a response and then calls next, later code may attempt a second response. If it does neither, the request remains pending. A middleware function must have a clear branch outcome.

7. Body parsing and content types

express.json() parses matching JSON request bodies and places the decoded value in req.body. express.urlencoded() handles conventional URL-encoded form bodies. Parsers must be registered before routes that need their output.

Parsing proves only that the body follows the selected grammar. Validate object shape, required fields, types, lengths, ranges and cross-field rules afterward. Reject unexpected content types rather than interpreting bytes ambiguously. Set body-size limits appropriate to the endpoint.

File uploads use multipart processing middleware rather than the JSON parser. Uploaded names, media declarations and extensions remain untrusted.

8. Resource-oriented routing

OperationRouteTypical successful status
List coursesGET /courses200
Retrieve courseGET /courses/:id200
Create coursePOST /courses201
Replace coursePUT /courses/:id200 or 204
Partially update coursePATCH /courses/:id200 or 204
Delete courseDELETE /courses/:id204

These are conventions, not a substitute for a written contract. A creation response can include Location for the new resource. A 204 response has no content and should not carry a JSON body. Use nouns in resource URLs and let HTTP methods express common operations.

9. Validation and status selection

Different failures deserve different handling:

SituationCommon status
Malformed JSON400
Syntactically valid but unacceptable representation400 or 422 according to contract
Missing or unacceptable authentication401
Authenticated but forbidden operation403
Missing resource404
State conflict, such as duplicate unique value409
Unexpected server failure500

Do not expose database messages or stack traces to clients. Stable error response fields help front ends display useful feedback and help tests assert behavior.

10. Controllers, services and repositories

A controller translates HTTP concepts into an application call and translates the result back into HTTP. A service enforces business operations without depending on Express. A repository or data-access object isolates persistence.

async function createCourseController(req, res, next) {
  try {
    const command = validateCourseInput(req.body);
    const course = await courseService.create(command, req.user);
    res.status(201).location(`/courses/${course.id}`).json(course);
  } catch (error) {
    next(error);
  }
}

This separation makes business rules reusable and unit-testable. It does not require one file per tiny function; boundaries should reflect responsibilities and change patterns.

11. Async errors and centralized handling

Error middleware has four declared parameters: (error, req, res, next). It belongs after routes and ordinary middleware.

app.use((error, req, res, next) => {
  if (res.headersSent) return next(error);
  const status = Number.isInteger(error.status) ? error.status : 500;
  res.status(status).json({
    error: status === 500 ? 'Internal server error' : error.message
  });
});

Exact async propagation behavior depends on the Express major version and handler style. Use a consistent project pattern that ensures rejected operations reach centralized handling. Check headersSent because an error after streaming begins cannot be converted into a fresh ordinary response.

An early return res.status(...).json(...) prevents later statements in the same handler from sending again. The return is a JavaScript control-flow decision; the response method itself does not magically stop the function.

12. Not-found handling

A final ordinary middleware can represent unmatched routes:

app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

Register it after every intended route or it will intercept requests before they reach them. Distinguish “route does not exist” from “route exists but the requested database record does not.” Both may return 404, but they arise at different layers and need different tests.

13. Authentication and authorization

Authentication establishes an identity; authorization decides whether that identity may perform this operation on this resource. Authentication middleware can establish req.user. Authorization should occur close to the protected operation and must account for ownership, roles, state and tenant boundaries.

Hiding a button in the client is a usability choice, not authorization. Every protected server endpoint must enforce permission independently. Avoid revealing whether a sensitive resource exists when that distinction would leak information.

14. Static files and server-rendered views

express.static(directory) serves files from a directory according to middleware configuration. Mount it at an intentional URL prefix and do not expose source, environment or upload directories accidentally.

A configured template engine renders a view with data:

app.get('/profile', requireUser, async (req, res, next) => {
  try {
    const profile = await profileService.get(req.user.id);
    res.render('profile', { profile });
  } catch (error) { next(error); }
});

Understand the engine's escaping rules. Deliberately rendering raw untrusted HTML creates injection risk. Views format data; they should not open their own database connections or duplicate business rules.

15. Security and operational middleware

Production concerns include:

  • request body and upload limits;
  • input validation and output encoding;
  • secure cookies and CSRF protection for cookie-authenticated state changes;
  • rate limiting where abuse is possible;
  • trusted proxy configuration before relying on forwarded addresses or protocol;
  • security headers and a controlled CORS policy;
  • structured request identifiers and logs;
  • graceful shutdown and health/readiness behavior.

Middleware packages help implement policy but do not remove the need to understand configuration. Ordering matters: authentication must run before middleware that requires identity; parsing must run before body validation; error handling comes after operations that can fail.

16. Complete API example

const courses = new Map();
let nextId = 1;

app.post('/courses', (req, res) => {
  const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
  if (title.length < 3) {
    return res.status(400).json({ error: 'Title must contain at least 3 characters' });
  }

  const course = { id: nextId++, title };
  courses.set(course.id, course);
  return res.status(201).location(`/courses/${course.id}`).json(course);
});

app.get('/courses/:id', (req, res) => {
  const id = Number(req.params.id);
  if (!Number.isSafeInteger(id) || id < 1) {
    return res.status(400).json({ error: 'Invalid course id' });
  }
  const course = courses.get(id);
  if (!course) return res.status(404).json({ error: 'Course not found' });
  return res.json(course);
});

The example validates decoded input, distinguishes invalid identity from missing resource, uses creation semantics, sets a location and stops each response branch explicitly.

17. Practical considerations

  1. A route is identified by method and path, not path alone.
  2. Path parameters, query parameters and body data occupy different locations.
  3. Middleware runs in registration order.
  4. Parsed input remains untrusted.
  5. A middleware branch must respond or continue.
  6. Error middleware has four parameters and belongs after routes.
  7. Sending a response does not automatically return from the JavaScript function.
  8. A 204 response contains no response body.
  9. Authentication establishes identity; authorization grants or denies an operation.
  10. A not-found middleware registered too early intercepts valid routes.

Continue learning

Related notes

Put this topic into timed practice

Open mock tests when you want full-exam pacing, or keep drilling in practice mode.