Phase 2 · Web Servers & HTTP APIsModule 10~58 min read

Requests, Responses & Native Routing

Read bounded request bodies, validate JSON, extract route parameters, and organize a native Node.js server without a framework.

What you'll learn

A useful HTTP server must do more than return one fixed response. You will consume streamed bodies with a limit, validate JSON, extract dynamic parameters, and separate routing from response formatting without a framework.

By the end of this lesson, you'll be able to:

  • Read a request body without trusting its size
  • Distinguish malformed JSON from invalid domain input
  • Match static and parameterized native routes
  • Centralize JSON responses and HTTP errors

Core mental model

Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.

ConceptWhat it meansDecision rule
Incoming streamRequest body bytes arrive over timeAccumulate only bounded small JSON bodies; use streaming for large payloads
Route matchMethod and path select a handler plus parametersMatch normalized path segments, never arbitrary substring checks
Boundary validationTransport input becomes trusted application dataParse syntax first, then validate shape and domain rules
Response helperOne function applies status, headers, and serialization consistentlyCentralize protocol details without hiding handler intent

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. Define route contracts before matcher code
  2. Apply a request-size limit before parsing
  3. Parse JSON and translate syntax failures into 400 responses
  4. Validate required fields and types
  5. Dispatch to a focused handler
  6. Send one consistent response and return immediately
  7. Exercise missing, malformed, oversized, and unknown-route requests

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

Guided code lab

Read a bounded JSON body

The request is async iterable. Counting bytes while reading prevents a client from making the process buffer an unlimited payload.

read-json.js
export async function readJson(request, limit = 1_000_000) {
  const chunks = [];
  let size = 0;

  for await (const chunk of request) {
    size += chunk.length;
    if (size > limit) {
      const error = new Error("Request body is too large");
      error.status = 413;
      throw error;
    }
    chunks.push(chunk);
  }

  try {
    return JSON.parse(Buffer.concat(chunks).toString("utf8"));
  } catch (cause) {
    throw Object.assign(new Error("Malformed JSON", { cause }), { status: 400 });
  }
}

Create a consistent JSON response helper

The helper serializes once, calculates the byte length, and applies the same media type everywhere.

respond.js
export function sendJson(response, status, value) {
  const body = JSON.stringify(value);
  response.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": Buffer.byteLength(body),
  });
  response.end(body);
}

export function sendProblem(response, status, message) {
  sendJson(response, status, { error: { status, message } });
}

Match a dynamic task route

URLPattern-like complexity is unnecessary for one route. Decode one segment and keep method checks explicit.

router.js
import { sendJson, sendProblem } from "./respond.js";

export async function route(request, response) {
  const url = new URL(request.url, "http://localhost");
  const match = url.pathname.match(/^/tasks/([^/]+)$/);

  if (request.method === "GET" && match) {
    const id = decodeURIComponent(match[1]);
    sendJson(response, 200, { id, title: "Learn native routing" });
    return;
  }

  sendProblem(response, 404, "Route not found");
}

Production practice

Limit early

Reject oversized input while bytes arrive; a Content-Length header is useful but cannot be blindly trusted.

Separate failures

Malformed JSON, invalid fields, missing resources, and internal defects need different statuses and messages.

Keep routing boring

A small native router is educational; adopt a maintained router or framework before recreating complex matching.

Common failure mode

Listening only for data and end without handling aborted and error conditions can leave work hanging or operate on incomplete bodies. Async iteration and explicit limits simplify safe consumption.

Independent workshop

Extend the native server into an in-memory task API supporting list, create, retrieve, and delete.

Your finished workshop must include:

  • A bounded JSON parser
  • Static and parameterized routes
  • 201, 204, 400, 404, 405, and 413 outcomes where appropriate
  • One response format for errors
  • Tests with curl or a small fetch client

Definition of done

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

Recap & quick check

Key takeaways

  • HTTP request bodies are streams
  • Limits belong before parsing
  • Syntax and domain validation are separate
  • Routes match method plus path
  • Response helpers make protocol behavior consistent

Quick check

1. Why count body bytes while reading?

2. What status fits malformed JSON?

3. What selects a route?

4. When should native hand-written routing give way to a framework?

Next: Express 5 Fundamentals & Routers