Node.js Backend — What the Framework Hides
Stand Up an API With Node's Standard Library
Goal
Build a small order API with only the Node standard library, without Nest. It has eight steps: a routing table, input validation, output shape, dependency injection, and the event loop.
Why do it this way
The lab Pod has only DNS open, so npm install does not work. But that constraint actually
suits this course: if you make by hand the judgments that a framework makes for you,
then when you later read Nest you can see what each mechanism is doing on your behalf.
Format
Create /root/work/api/app.mjs and export the following.
handle(req):reqis{method, path, body, headers}, and what you return is{status, body}. It does not open a real socket.createApp({store}): you build it in step 6.
The reason it does not open a socket is that grading must not be shaken by port conflicts or startup timing. A test is only useful if a failure means a real failure.
Set up the skeleton of the server
Run mkdir -p /root/work/api, then in app.mjs put export async function handle(req). req is {method, path, body, headers}, and what you return is {status, body}. For /healthz, return 200 and {status:'ok'}.
handle('/healthz') returns 200 and {status:'ok'}
Build the routing table: tell 404 from 405
Keep the list of paths in an array, first collect the entries whose path matches, and then look at the method. If there is no such path, return 404; if the path exists but the method does not, return 405. GET /items returns an array.
A path that does not exist gives 404; another method on an existing path gives 405
Validate the input and say why it is wrong
POST /items accepts {name: string, qty: number}. If it is wrong, return 400 with {errors:[...]} describing which field is wrong and why. If it is valid, return 201.
If qty is a string, 400 with qty in errors; if valid, 201
Separate the stored shape from the shape you send out
The store has secret, but the response must not. Do not delete it in each handler; choose the fields to send out in one place. That is an allowlist.
The GET /items response has no secret and has id, name, and qty
Give 404 for what does not exist
Build GET /items/:id. If it does not exist, return 404. A response with status 200 that carries {error:...} is read by the client as success. The status code is the contract.
A nonexistent id gives 404; an existing id gives 200 and that item
Receive the store by injection
Write export function createApp({store}) and make the handlers use only that store. If you refer to a global directly, you cannot swap it out in the next step.
The store passed to createApp({store}) is actually used, and the output shape is preserved
Do not block the event loop
Build GET /slow, but make it take at least 40ms. If you burn time with a while loop, even timers stop in the meantime. Yield with await new Promise(r => setTimeout(r, 50)).
While four requests are handled concurrently, a 5ms timer keeps ticking
Test the contract without starting a server
In app.test.mjs, write at least two tests that run with node --test. Test by injecting a fake store with createApp. You must not open a port.
node --test passes at least 2 tests and does not start a server