LLMSkillHub
mirroredCopied from willdady/platypus under MIT. willdady has not claimed this page, and is not affiliated with LLM SkillHub.

willdady / api-conventions

Response-body conventions for the Platypus backend API — the `error` key for failures, the `message` key for 2xx status messages, and when to throw a typed error instead of returning a response. Use when adding or changing a backend route or its tests.

0
SKILL.md
2.6 KB

API Response Conventions

The two keys

error carries a failure. message carries a 2xx status line for an
operation that has no resource to return.

return c.json({ error: "Workspace not found" }, 404);
return c.json({ message: "Board deleted" });

Never message on a 4xx/5xx — that is the one crossing the codebase does not
make.

A delete or action with nothing to return answers 200 { message } — never a
bodiless 204, and never { success: true }.

The error seam

Five cross-cutting failures are thrown, not returned. A single onError
seam maps each to its status and emits the same { error } body, so a domain
rule states its message once instead of once per caller:

ThrowBecomes
NotFoundError404
ValidationError400
LockedError403
ConflictError409
UnsupportedError501

A Postgres unique violation also maps to 409 — detect it through the shared
helper rather than re-reading the driver's error shape. So does a foreign-key
violation, as a backstop only: a delete guarded by a restrict reference
should throw a ConflictError naming what still uses the row first.

if (!row) throw new NotFoundError("Card not found");
throw new ValidationError("Invalid user assignee");

A resource referenced in the request body that isn't in this Organization, or
isn't Shared, is a NotFoundError (404) naming what couldn't be used — not a
422, and no extra detail fields beside error.

Throwing is what lets one rule serve more than one surface: the Kanban rules
answer both the HTTP routes and the Agent tool set from a single place. Reach
for the seam whenever the failure is one of those five; a 4xx that only one
route can produce still answers inline with c.json({ error }, status). Both
are current and roughly equally common — the seam is not a migration you
should finish.

Test assertions

Assert against the key the response actually uses:

expect(body.error).toBe("Invalid user assignee");
expect(await res.json()).toEqual({ message: "Board deleted" });

A service test covering a thrown failure asserts the error, not a body:

await expect(fn()).rejects.toThrow(NotFoundError);