DEV Community

Riley Zhang
Riley Zhang

Posted on

Weekend Build Log: Demo the Failure Path First

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.

  1. POST /check reads a JSON object with url.
  2. Invalid bodies return HTTP 400 and one frozen error.
  3. Valid bodies return HTTP 200 and a tiny OK payload.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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",
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Run the tests before any HTTP server exists:

export PYTHONPATH=.
python -m unittest tests.test_contract -v
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Start the server in one terminal:

export PYTHONPATH=.
python app.py
Enter fullscreen mode Exit fullscreen mode

Run the smoke in another terminal:

chmod +x scripts/smoke.sh
./scripts/smoke.sh
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

  1. Dump /tmp/bad.json and /tmp/good.json to the terminal.
  2. Diff each file against contract/errors.py by eye.
  3. If the handler drifted, revert the handler, not the test.
  4. If the contract changed, revert the contract immediately.
  5. Re-run make test before you re-run make 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
Enter fullscreen mode Exit fullscreen mode

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.

  1. No live HTTP fetch of the submitted URL.
  2. No database and no user accounts.
  3. No retry policy, queue, or worker process.
  4. 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)