Course outline · 0% complete

0/29 lessons0%

Course overview →

Assembling the Core, End to End

lesson 9-2 · ~14 min · 28/29

Every piece, one function

Below is the create-note path of the API, assembled from parts you have already built:

  • the session Map from lesson 7-2 (pretend s1 came from a login),
  • the repository arrays from lesson 6-2,
  • a validator in the lesson 5-3 style,
  • the error shape from lesson 4-3 and status codes from 3-3,
  • ordered like the lesson 5-2 pipeline: auth, then validation, then the handler.

Your job is handleCreateNote, the conductor. It checks the session, validates the body, creates the note, and returns { status, body }. Three TODOs, three pipeline stages. When the last line prints, you have written the core of a production API pattern, end to end.

The capstone handler

handleCreateNote runs the three pipeline stages in order: authenticate, validate, then do the work.

// auth (lesson 7-2): s1 is ada's live session
const sessions = new Map([["s1", { username: "ada" }]]);

// storage (lesson 6-2)
const notes = [];
let nextId = 1;

// validation (lesson 5-3)
function validateNote(body) {
  const errors = [];
  if (!body || typeof body.text !== "string" || body.text.length === 0) {
    errors.push({ field: "text", message: "text is required" });
  }
  return errors;
}

function handleCreateNote(req) {
  const session = sessions.get(req.sessionId);
  if (!session) {
    return { status: 401, body: { error: { code: "unauthorized" } } };
  }

  const errors = validateNote(req.body);
  if (errors.length > 0) {
    return { status: 400, body: { error: { code: "invalid_input", details: errors } } };
  }

  const note = { id: nextId, author: session.username, text: req.body.text };
  nextId++;
  notes.push(note);
  return { status: 201, body: note };
}

console.log(JSON.stringify(handleCreateNote({ sessionId: "s1", body: { text: "ship it" } })));
console.log(JSON.stringify(handleCreateNote({ sessionId: "s9", body: { text: "hi" } })));
console.log(JSON.stringify(handleCreateNote({ sessionId: "s1", body: {} })));
console.log("notes stored: " + notes.length);

Output

{"status":201,"body":{"id":1,"author":"ada","text":"ship it"}}
{"status":401,"body":{"error":{"code":"unauthorized"}}}
{"status":400,"body":{"error":{"code":"invalid_input","details":[{"field":"text","message":"text is required"}]}}}
notes stored: 1

Stage one is sessions.get(req.sessionId) with an early return of the 401 object when it comes back undefined. Nothing below that line has to wonder whether there is a user, which is what makes the rest of the function readable.

Stage two collects errors from validateNote and returns 400 with details: errors when any exist. The details array is what lets a form highlight the offending field, which is the whole argument from lesson 4-3.

Stage three builds the note with id: nextId and author: session.username, then increments, pushes, and returns 201 with the note. The author comes from the session rather than the body, and that single choice is what stops a client from creating notes under someone else's name.

The order is not stylistic. Auth before validation means an unauthenticated request never learns which fields your API expects, and validation before work means no half-built record reaches storage.

Look at the last output line, which says one note stored after three calls. Two requests were rejected and neither left a trace in the array, so the rejections happened before any mutation, which is exactly the property you want from a pipeline.

Every return has the same { status, body } shape, and the 401 and 400 bodies both use the lesson 4-3 error envelope. A handler that returns plain data instead of writing to a response object is also trivially testable, since a test can call it directly and inspect the returned object with no server running.

What remains before a real deployment

The remaining work is wiring. Everything decision-shaped is done and already exercised by the output above.

Express contributes plumbing rather than decisions. express.json() parses request bodies into req.body, the router maps POST /notes to the handler, and res.status(...).json(...) sends what the handler returned.

The session id rides an httpOnly cookie instead of arriving as a plain req.sessionId field, which is the lesson 7-2 detail that keeps page scripts from reading it.

The array repository becomes parameterized SQL from lesson 6-3, and the handler does not change shape when it does. That is the point of the persistence boundary from lesson 6-2, since notes.push becoming an INSERT is a swap behind a stable interface.

Unit 8 supplies the operational layer around all of it: environment config validated at boot, structured logs, one error boundary that hides internals, and a limiter on /login.

PieceWhere it came fromStill to wire
pipeline orderlesson 5-2done
error shapelesson 4-3done
body parsingExpressexpress.json()
session transportlesson 7-2httpOnly cookie
storagelesson 6-3parameterized SQL
config, logs, limitsunit 8env plus middleware

Frameworks change and this core does not, which is the durable takeaway. A Fastify or Hono version of the same API rewrites the wiring column and leaves the handler logic intact.

Keeping a list response bounded

Pagination.

GET /notes?page=1&perPage=20 returns one bounded slice plus meta so the client can render page controls, using exactly the paginate function from lesson 4-2 with toPositiveInt from lesson 5-3 guarding the query params.

The guard matters as much as the slicing. A caller who sends perPage=100000 is asking for the same unbounded response by a different route, so a maximum has to be clamped server-side.

Returning 50,000 notes fails in several places at once, which is worth naming. The database reads every row, the server serializes megabytes of JSON, the network carries it, and the browser tries to render it, so the cost lands on all four.

The meta object carried page, perPage, total, and totalPages, and that is what turns a slice into something a UI can navigate. Without total the client cannot draw a page count or know when it has reached the end.

One thing is still missing before this API is safe to ship, which is making sure users only ever touch their own notes. That is the next and final lesson.