| name | api-contract |
| description | Enforces the shared HTTP API contract defined in spec 0002. Use when implementing or reviewing any endpoint handler in servers/go, servers/rust, servers/python, servers/nodejs, or servers/dotnet. Trigger phrases: "implement endpoint", "add handler", "api contract", "/api-contract", or when editing any handlers.* or routes.* file in the servers/ directory.
|
| applyTo | ["servers/**"] |
API Contract (spec 0002)
All five servers must produce bit-for-bit identical HTTP behavior.
Any divergence — wrong status code, wrong JSON shape, wrong Content-Type —
makes benchmark data incomparable and invalidates the study.
Quick reference
| Method | Path | Success | Error |
|---|
| GET | /items | 200 […] or [] | — |
| GET | /items/:id | 200 {item} | 404 {"error":"not found"} |
| POST | /items | 201 {item} | 400 {"error":"name is required"} |
| DELETE | /items/:id | 204 (no body) | 404 {"error":"not found"} |
Data model
{
"id": 1,
"name": "item-001",
"created_at": "2026-01-01T00:00:00Z"
}
created_at format rules:
- ISO-8601 UTC
- Always ends with uppercase
Z (not +00:00, not lowercase z)
- No milliseconds (seconds precision only):
2026-01-01T00:00:00Z
Status codes — the non-negotiables
GET /items → 200 always (empty list = 200 [], never 404)
GET /items/:id → 200 found | 404 not found
POST /items → 201 created | 400 missing name
DELETE /items/:id → 204 deleted | 404 not found
204 DELETE must have no body. Sending {} or null is wrong.
POST returns 201, not 200. The created item is in the response body.
Content-Type rules
- Every response except 204 must include
Content-Type: application/json
- 404 and 400 errors must also include
Content-Type: application/json
- No charset suffix is required but must not contradict JSON (e.g.
charset=utf-8 is acceptable)
Error body shape
All errors use exactly this shape — no variations:
{"error": "not found"}
{"error": "name is required"}
Do NOT use:
{"message": "..."} — wrong key
{"error": {"code": 404}} — wrong value type
{"errors": [...]} — wrong key, plural
null — must be a JSON object
POST /items — request body rules
{"name": "my-item"}
name must be a non-empty string
- Missing
name → 400 {"error":"name is required"}
name is "" (empty string) → 400 {"error":"name is required"}
name is null → 400 {"error":"name is required"}
- Extra fields in the body are ignored (do not return 400 for them)
created_at is set server-side as UTC now; the client does not send it
GET /items — ordering
Return items in insertion order (ascending by id). Do not sort differently.
Database constraints
- Table:
items, schema is defined in spec 0003 (setup_db.py seeds 1000 rows)
- Never pre-load rows into memory — query the live DB on each request (RF7)
DB_PATH env var sets the path (default: ../../data/benchmark.db relative to binary)
PORT env var sets the bind port (default: 8080)
Anti-patterns (common mistakes by language)
All languages
- ❌ Returning 200 on POST instead of 201
- ❌ Returning a body on 204 DELETE
- ❌
created_at using local timezone instead of UTC
- ❌
created_at with milliseconds: 2026-01-01T00:00:00.000Z
- ❌
created_at using +00:00 instead of Z
- ❌ Missing
Content-Type: application/json on error responses
Go
- ❌ Not calling
w.Header().Set("Content-Type","application/json") before w.WriteHeader()
- ❌ Using
http.Error() which sets text/plain; charset=utf-8
Rust (Axum)
- ❌ Returning
StatusCode::OK (200) from a POST handler that creates a resource
Python (FastAPI)
- ❌ Forgetting
status_code=201 in the @app.post decorator
- ❌ Pydantic serializing
datetime as 2026-01-01T00:00:00 without Z — ensure UTC and use .isoformat().replace('+00:00','Z')
Node.js (Fastify)
- ❌
reply.code(204).send({}) — must be .send() with no argument
- ❌ Not registering a JSON schema on the route, causing Fastify to skip serialization optimization
.NET (Minimal API)
- ❌
Results.Ok(item) on a POST — use Results.Created("/items/{id}", item)
- ❌ AOT:
JsonSerializer without source-generated context returning wrong field names