Saturday morning hits 10:14 with an empty repo.
You promised a live demo at two o'clock.
The coffee on your desk is already cold.
A model window waits on the second screen.
It offers a full product in one paste.
You should refuse that offer until the 400 exists.
Most weekend crashes are bad input, not missing features.
Friends will send empty JSON. They always do.
A green 200 with no failure path is a costume.
The scene you are actually in
You are building a tiny URL check API.
One POST /check route. One JSON body. One honest error.
That is the whole afternoon, not a starter for a platform.
Last demo you showed a clean happy path first.
Someone posted {"url": ""} and the process died.
The room remembered the crash, not your feature list.
Today you invert the order on purpose.
The 400 is the first feature. The 200 is leftover glue.
If time dies, the failure path still stands on stage.
Cut the scope in writing
Write the cut before you touch a prompt.
Paper beats a chat thread when the clock is loud.
If a task is missing here, it is not this weekend.
-
POST /checkreads a JSON object withurl. - Invalid bodies return HTTP 400 and one frozen error.
- Valid bodies return HTTP 200 and a tiny OK payload.
- You do not fetch the remote URL during the demo.
Fetching looks serious. Fetching also hangs at two o'clock.
Auth looks mature. Auth also explodes the diff.
You skip both. The error body is the product today.
Artifact: a hand-written error contract
Do not let the model invent the 400 shape.
You type the file. Tests import that file.
The handler imports the same file. Nobody "improves" it.
Proposed weekend loop. Run it locally before you demo.
mkdir -p contract tests scripts
touch contract/__init__.py tests/__init__.py
contract/errors.py:
# Hand-written. Do not regenerate this module.
ERROR_INVALID_PAYLOAD = {
"error": "invalid_payload",
"field": "url",
"hint": "url must be an https URL",
}
OK_PAYLOAD = {
"ok": True,
"checked": "syntax",
}
That module is the demo script in disguise.
If a patch rewrites it, you revert the file and stop.
Green output against a new shape is not a win.
Numbered build, ninety minutes
1. Pin the contract with tests you typed
tests/test_contract.py:
import unittest
from contract.errors import ERROR_INVALID_PAYLOAD, OK_PAYLOAD
class ContractTests(unittest.TestCase):
def test_error_keys_are_frozen(self):
self.assertEqual(
set(ERROR_INVALID_PAYLOAD),
{"error", "field", "hint"},
)
def test_error_values_do_not_drift(self):
self.assertEqual(
ERROR_INVALID_PAYLOAD["error"],
"invalid_payload",
)
self.assertEqual(ERROR_INVALID_PAYLOAD["field"], "url")
def test_ok_payload_stays_tiny(self):
self.assertEqual(
OK_PAYLOAD,
{"ok": True, "checked": "syntax"},
)
if __name__ == "__main__":
unittest.main()
Run the tests before any HTTP server exists:
export PYTHONPATH=.
python -m unittest tests.test_contract -v
You now own a gate the model did not author.
Fail this gate and you do not open the chat tab.
Passing it does not mean the service works yet.
2. Write the smallest handler that can fail
Stdlib only. No framework debate. No extra install.
app.py:
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
from urllib.parse import urlparse
from contract.errors import ERROR_INVALID_PAYLOAD, OK_PAYLOAD
HOST = "127.0.0.1"
PORT = 8787
def check_url(payload):
if not isinstance(payload, dict):
return 400, ERROR_INVALID_PAYLOAD
url = payload.get("url")
if not isinstance(url, str):
return 400, ERROR_INVALID_PAYLOAD
parsed = urlparse(url)
if parsed.scheme != "https" or not parsed.netloc:
return 400, ERROR_INVALID_PAYLOAD
return 200, OK_PAYLOAD
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/check":
self._send(404, {"error": "not_found"})
return
length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(length)
try:
payload = json.loads(raw.decode("utf-8") or "null")
except json.JSONDecodeError:
self._send(400, ERROR_INVALID_PAYLOAD)
return
status, body = check_url(payload)
self._send(status, body)
def _send(self, status, body):
data = json.dumps(body).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, fmt, *args):
return
if __name__ == "__main__":
HTTPServer((HOST, PORT), Handler).serve_forever()
Notice the 404 is not the frozen contract.
Only the 400 body is shared with tests.
Keep extra codes boring so the demo stays sharp.
3. Smoke the 400 first, then the 200
scripts/smoke.sh:
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
export PYTHONPATH=.
BASE="${BASE_URL:-http://127.0.0.1:8787}"
bad=$(curl -sS -o /tmp/bad.json -w "%{http_code}" \
-H "Content-Type: application/json" \
-d '{"url":"ftp://example.com"}' \
"$BASE/check")
test "$bad" = "400"
python - <<'PY'
import json
from contract.errors import ERROR_INVALID_PAYLOAD
body = json.load(open("/tmp/bad.json"))
assert body == ERROR_INVALID_PAYLOAD, body
print("400 contract held")
PY
good=$(curl -sS -o /tmp/good.json -w "%{http_code}" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/x"}' \
"$BASE/check")
test "$good" = "200"
python - <<'PY'
import json
from contract.errors import OK_PAYLOAD
body = json.load(open("/tmp/good.json"))
assert body == OK_PAYLOAD, body
print("200 payload held")
PY
Start the server in one terminal:
export PYTHONPATH=.
python app.py
Run the smoke in another terminal:
chmod +x scripts/smoke.sh
./scripts/smoke.sh
If the 400 fails, you do not show the 200.
Demo order is the whole point of this cut.
A passing happy path after a broken error is theater.
4. Only then ask a model for glue
Now the model may write a Makefile or README.
It may not edit contract/errors.py.
It may not weaken scripts/smoke.sh to stay green.
Paste a short prompt with the freeze in it:
Keep contract/errors.py unchanged.
Keep scripts/smoke.sh failing on any 400 drift.
Add a Makefile with targets: test, serve, smoke.
Do not add fetching, auth, or HTML.
A Makefile worth accepting looks like this:
.PHONY: test serve smoke
export PYTHONPATH := .
test:
python -m unittest tests.test_contract -v
serve:
python app.py
smoke:
./scripts/smoke.sh
If you want a spare remote workbench for that loop, MonkeyCode can host it.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode offers free model access and a free server option.
Treat both as a spare workbench, not as production hosting.
You still paste the same prompt. You still run the same smoke.
When the smoke turns red
Do not start by asking the model what went wrong.
Print the bodies. Diff them against the frozen file.
The repair order matters more than the stack trace.
- Dump
/tmp/bad.jsonand/tmp/good.jsonto the terminal. - Diff each file against
contract/errors.pyby eye. - If the handler drifted, revert the handler, not the test.
- If the contract changed, revert the contract immediately.
- Re-run
make testbefore you re-runmake smoke.
A common failure is pretty-printed JSON with extra keys.
Another is a rewritten hint that sounds friendlier.
Friendly is not the contract. Exact bytes are the contract.
python - <<'PY'
import json
from contract.errors import ERROR_INVALID_PAYLOAD
got = json.load(open("/tmp/bad.json"))
print("got ", sorted(got.items()))
print("want", sorted(ERROR_INVALID_PAYLOAD.items()))
PY
If those two lines disagree, the demo is not ready.
Do not narrate the mismatch as a style issue.
Call it a broken feature and fix the handler.
What you skip on purpose
Write the skip list in the README before the demo.
Future-you will try to "finish" the toy on Sunday night.
The list is a fence, not a backlog you must burn.
- No live HTTP fetch of the submitted URL.
- No database and no user accounts.
- No retry policy, queue, or worker process.
- No frontend, tokens, cookies, or rate limits.
Crawling a real site introduces jitter you cannot defend.
Accounts introduce password reset theater you cannot finish.
A form hides status codes from the people you must convince.
You skipped them because the 400 already proves existence.
Existence is the weekend bar. Polish is a weekday job.
Say that out loud when someone asks for one more thing.
Decision table for the remaining hour
| Idea that pops up | Ship today? | Why |
|---|---|---|
| Pretty HTML form | No | Demo is curl. Forms hide status codes. |
| Real site crawl | No | Network jitter becomes your product. |
| Model-generated tests | No | The contract must outrank the model. |
| Extra error codes | No | One frozen 400 is easier to defend. |
| Makefile plus README | Yes | You need a one-command rerun. |
| Log redaction | Later | No secrets live in this payload. |
If a row says No, you close the tab.
Do not renegotiate the table after 1:30 p.m.
The table is part of the demo, not private notes.
Limitations you should say out loud
This workflow does not measure latency under load.
It does not prove the URL is safe or reachable.
It does not replace review of any model patch.
A frozen 400 can still be a bad 400.
If your hint misleads users, you shipped a clean lie.
Read the JSON. Do not worship the green smoke.
Syntax checks are not security reviews.
https:// plus a host is not a safe fetch target.
Do not pretend otherwise when a friend asks.
Free model access will not choose scope for you.
A free server is not an SLA and may change.
Keep the contract and the smoke in your own git repo.
Who should not use this approach
Do not use this cut if you must ship real fetching today.
Do not use it for regulated data or paid user traffic.
Do not use it if your reviewer needs load numbers.
Security teams need more than a scheme check.
On-call teams need timeouts, metrics, and runbooks.
This article is a weekend gate, not a platform guide.
Teams with an existing public error catalog should ignore this.
Do not fork a second 400 shape beside a living standard.
Match the catalog you already owe, or do not start.
The two o'clock demo
You open a terminal. You run the 400 curl first.
The body matches the file you wrote that morning.
Then you run the 200. It is boring. Boring is shippable.
Someone asks for crawling. You point at the skip list.
The demo ends on time. The repo stays small.
Monday-you can add fetch behind a flag, if anyone still cares.
Top comments (0)