Stop grading the generated server. Grade whether a stranger can boot it on a pinned runtime without guessing Node, ports, or hidden env vars. That is the whole lab.
AI agents are getting fluent at writing Express handlers. They are still reckless about the machine those handlers will live on. You have seen the failure: green on a laptop, red in CI, mysterious in a classmate's demo. Why do we keep scoring the souvenir instead of the replay?
This is a proposed bootcamp lab, not a claim that I already ran it on a giant cohort. Steal the worksheet. Change the stack. Keep the rule: no runtime contract, no grade.
The problem the lab is actually about
Students now paste a ticket into a coding assistant and get a folder that looks like a service. Tests pass locally. Then the grader clones the repo on a different OS, a different Node, a missing .env, and a port already taken by Discord. Suddenly the agent looks brilliant and the student looks unlucky.
Luck is not a learning outcome.
The agent did not fail at JavaScript. It failed at environment honesty. It assumed node meant whatever was on PATH. It assumed PORT=3000 was free. It assumed fetch existed. It assumed the grader would silently install whatever npm i felt like that afternoon.
Sound familiar? Same class of bug as an agent inventing AWS resource names. Different surface.
Lab goal (one sentence)
Ship a tiny Node HTTP service and a machine-readable runtime contract so a grader can replay boot, test, and a single happy-path request on a pinned server — not on the author's laptop.
If the grader has to ask you a question, the contract is incomplete.
What you are allowed to use
You may use an AI coding assistant. You should. The point is not abstinence.
You may not hide the environment. If the model writes npm start and you never freeze Node, that is a lab failure, not a style issue.
For students who do not have a cloud bill and cannot share a home laptop with a grader, a shared free server plus free model access is the boring, useful setup. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is one option that currently offers free model access and a free server option; I am not inventing model names, quotas, or hardware here, and I am not asking you to treat any vendor as the subject of the lab. The subject is the contract file.
If you already have a Codespace, a campus VM, or a Raspberry Pi under the desk, use that. The checklist does not change.
Setup (30–45 minutes, before any feature code)
Work in a clean directory. Do not generate the service yet. Seriously. The agent will try to skip this. Do not let it.
mkdir runtime-contract-lab && cd runtime-contract-lab
git init
node -v
npm -v
uname -s
Create three files by hand, even if a model offers to "just scaffold everything":
-
runtime-contract.json— the source of truth -
contract.test.mjs— the grader's first command -
README.md— human translation of the JSON, not a substitute for it
Proposed contract shape (treat this as a lab spec, not a standard):
{
"contractVersion": 1,
"runtime": {
"language": "javascript",
"node": { "major": 22, "exact": null },
"packageManager": "npm@10"
},
"boot": {
"install": "npm ci",
"start": "node server.mjs",
"healthPath": "/health",
"portEnv": "PORT",
"defaultPort": 3000,
"readyTimeoutMs": 5000
},
"env": {
"required": ["PORT"],
"forbidden": ["AWS_SECRET_ACCESS_KEY", "OPENAI_API_KEY"],
"notes": "No paid APIs in the core path. Health must work offline."
},
"assumptions": []
}
That empty assumptions array is a trap. The agent will leave it empty. You will not.
Checkpoint 0 — freeze the machine, not the vibes
On the same host the grader will use, record reality:
node -p "process.version"
node -p "process.platform"
printf 'PORT=3000\n' > .env.example
If you cannot get a shared host, stop. Do not start coding. Pair with someone who can, or wait for lab staff to hand you a URL. Local-only work is practice, not a submission.
The tiny service (deliberately boring)
Requirements, not a product pitch:
-
GET /healthreturns200and{"ok":true} -
GET /whoamireturns Node version and whetherfetchexists - No database. No auth. No "while we are here" features.
- Must listen on
process.env.PORT - Must exit non-zero if
PORTis missing
A starter the student (or the model) can complete:
// server.mjs — complete this; do not add a framework unless the contract says so
const port = process.env.PORT;
if (!port) {
console.error("PORT is required");
process.exit(1);
}
const server = Bun; // trap: delete this line. You are on Node.
Yes, I put a trap in the snippet. If your agent copies it blindly, that is data. Write the failure into assumptions or fix the file. Do not shrug.
A fair implementation looks closer to this:
// server.mjs
import http from "node:http";
const port = process.env.PORT;
if (!port) {
console.error("PORT is required");
process.exit(1);
}
const server = http.createServer((req, res) => {
if (req.url === "/health" && req.method === "GET") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.url === "/whoami" && req.method === "GET") {
res.writeHead(200, { "content-type": "application/json" });
res.end(
JSON.stringify({
node: process.version,
fetch: typeof fetch === "function",
platform: process.platform,
})
);
return;
}
res.writeHead(404);
res.end();
});
server.listen(Number(port), () => {
console.log(`listening on ${port}`);
});
Keep it this small on purpose. If the model opens a React app, you failed the prompt, not the runtime.
The grader's first command
Do not make humans read a novel. Make Node fail loudly.
// contract.test.mjs
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
const contract = JSON.parse(await readFile("./runtime-contract.json", "utf8"));
test("contract declares a Node major that matches this process", () => {
const actualMajor = Number(process.versions.node.split(".")[0]);
assert.equal(contract.runtime.node.major, actualMajor);
});
test("boot.install is npm ci, not a vibes command", () => {
assert.equal(contract.boot.install, "npm ci");
});
test("core path forbids secret-shaped env vars", () => {
for (const name of contract.env.forbidden) {
assert.equal(process.env[name], undefined);
}
});
test("assumptions is not an empty shrug", () => {
assert.ok(Array.isArray(contract.assumptions));
assert.ok(contract.assumptions.length >= 3, "name three things the agent might have invented");
});
Run it on the pinned host:
npm ci
node --test contract.test.mjs
PORT=3000 node server.mjs &
curl -sS http://127.0.0.1:3000/health
curl -sS http://127.0.0.1:3000/whoami
If /whoami reports a Node major that is not in the contract, the lab is not done. Do not "fix" it by editing the test. Fix the contract or the host. Which one is cheaper? Usually the JSON.
Checkpoints (staff can grade these in order)
Use these as gates. Later work does not unlock earlier work.
- Host exists. Student posts a replay URL or SSH alias for a shared runtime. Laptop screenshots do not count.
-
Contract matches the host.
node --test contract.test.mjsis green on that host. -
Boot is copy-pasteable. A grader who has never opened the chat log can
npm ciand start the server from the contract alone. -
Health is offline. Unplug extra APIs.
/healthstill returns200. - Assumptions are ugly and specific. "Might need fetch" is a fail. "Node 18 has no global fetch; we pinned 22" is a pass.
-
Transcript exists. A short
lab-log.mdlists prompts, files touched, and commands run, in order. Not a novel. A receipt.
Miss gate 1 and I will not read your handlers. Harsh? Maybe. Faster than debugging three Node versions by emoji.
Stretch goals (optional, scored separately)
- Add
enginestopackage.jsonand makenpm cifail on the wrong major. - Add a
scripts/replay.shthat reads the contract and refuses to start ifnode -p process.versions.nodedisagrees. - Break the server on purpose on a second host (different major) and paste the error. That failure is the lesson.
- Replace the HTTP server with a CLI that prints the contract and exits. Same gates. Smaller surface.
Do not stretch into Kubernetes. If you need a mesh to prove you can listen on a port, the lab has already escaped.
Fair grading rubric (100 points)
Score the replay, not the chat charisma.
| Points | What I actually look at |
|---|---|
| 15 | Shared host is reachable by the grader without a meeting |
| 20 |
runtime-contract.json matches the host; tests green there |
| 15 |
PORT required; process exits non-zero when missing |
| 15 |
/health and /whoami behave as specified, no extra network |
| 15 |
assumptions has ≥3 concrete, falsifiable notes |
| 10 |
lab-log.md is a command/prompt receipt, not a diary |
| 10 | Stretch: engines field or replay script that fails closed |
Automatic zeros:
- Secrets in the repo or in the contract
- "Works on my machine" as the only evidence
- Contract that says Node 22 while
/whoamiprints v18 - Generated framework the ticket did not ask for
- Empty assumptions array
I will not grade spelling in the README if the JSON is honest. I will grade a beautiful README that lies about Node.
How the assistant is allowed to participate
Give the model the contract before the ticket. Then give it the ticket. Then freeze the contract again if the model tries to add Redis "just in case."
A prompt that has worked in dry runs of this worksheet:
You are not allowed to write server.mjs until you propose three edits
to runtime-contract.json. Each edit must be a fact about Node, PORT,
or install, not a feature. Wait for me to accept the JSON. Then write
the smallest http server that satisfies /health and /whoami.
If the model starts coding anyway, that is a student skill issue. Stop it. Paste the contract back. Ask: which line of JSON authorizes this file?
Free model access matters here because every student needs the same chance to iterate on that argument without a personal API key. A free server matters because the grader and the student must lose the argument against the same process.version. Paid clouds are fine. They are not required for the learning outcome.
Limitations (read these before you adopt the lab)
- This does not teach product design. The service is a fixture.
- This does not prove the model is "good." It proves the student can pin a machine.
- JSON will not save you from a host that mutates under your feet. Pin an image or refuse the host.
-
npm cineeds a lockfile. If you never commitpackage-lock.json, you did not pin installs. - Health checks can lie.
/healthreturning 200 while/whoamicrashes is still a fail under this rubric. - I have not published vendor benchmarks, token ceilings, or uptime promises. If a free tier moves, the contract still works on whatever host you still control.
Who should not use this approach
Skip this lab if you are teaching algorithms on paper. Skip it if your students have no network and no shared VM — the replay is the point. Skip it if the course already grades production Kubernetes. Skip it if you want to evaluate prompt style; this rubric will under-score a gorgeous chat that ships an unpinned runtime.
Staff on a two-hour workshop: use only checkpoints 1–4. Cut stretch. Cut /whoami if time explodes.
What I want back from you
Not a screenshot of a passing test on a stickered MacBook. A contract, a lockfile, a replay command, and three ugly assumptions.
Can your agent describe the computer, or only the code? If you need a no-card-required box to run the same checklist with a classmate acting as grader, MonkeyCode's free server option is one way to pin that replay. Steal the JSON either way.
Top comments (0)