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.
| Concept | What it means | Decision rule |
|---|---|---|
| Incoming stream | Request body bytes arrive over time | Accumulate only bounded small JSON bodies; use streaming for large payloads |
| Route match | Method and path select a handler plus parameters | Match normalized path segments, never arbitrary substring checks |
| Boundary validation | Transport input becomes trusted application data | Parse syntax first, then validate shape and domain rules |
| Response helper | One function applies status, headers, and serialization consistently | Centralize protocol details without hiding handler intent |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Define route contracts before matcher code
- Apply a request-size limit before parsing
- Parse JSON and translate syntax failures into 400 responses
- Validate required fields and types
- Dispatch to a focused handler
- Send one consistent response and return immediately
- Exercise missing, malformed, oversized, and unknown-route requests
Keep the feedback loop short
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.
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.
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.
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
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
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