API design
هذا المحتوى غير متوفر بلغتك بعد.
Most API mistakes I’ve made or reviewed came from designing endpoints before I understood the domain. Two rules that would have saved me years:
Design the contract before the code
The API contract is the product boundary. Every consumer — mobile app, admin panel, third party, the AI feature you’ll ship next quarter — depends on it. Change it casually and you break everyone.
I write the OpenAPI schema (or the tRPC procedure signature, or the GraphQL type) first, get it reviewed, then generate the types and write the handler. When the contract is the source of truth:
- Frontend can build against a mock before the backend is ready.
- Consumers see breaking changes at PR time, not in production.
- The types in my handler and the types in the client are the same types. Drift becomes structurally impossible, not culturally prevented.
The failure mode I’ve seen most: types are written by hand, kept “synced” by convention, and drift within a sprint. Don’t sync — generate.
Model state machines, not CRUD endpoints
Most business objects are not rows to be updated. They’re state
machines. A subscription is not “a row with a status field”; it’s
active → past_due → grace_period → cancelled → reactivated, with
rules about which transitions are legal from which state, and what
happens to related objects (renewals, refunds, entitlements) at each
one.
If your API is PATCH /subscriptions/:id { status: 'cancelled' },
you’ve handed every caller the ability to put your system in any state
they want, in any order. What you actually want is
POST /subscriptions/:id/cancel — an action verb, with the server
deciding what “cancel” means for the current state. The same instinct
applies on the frontend — see frontend architecture.
I learned this the hard way on Carizmo. The original subscription
model had washesRemaining and a nextWashDate, and it looked clean
in the ERD. What it didn’t model: what happens when a wash is
attempted but fails (car covered, gate locked, customer not home).
The row shape said “it happened or it didn’t.” Reality had five other
states. We ended up rewriting the subscription and job models to be
explicit state machines with named transitions. The lesson: if you
can’t draw the state diagram on a whiteboard, your API is going to
lie about what your system actually does.
A convention worth trying, not a rule
For internal APIs where I own both sides, I’ve had good results with
GET for reads and POST /resources/<verb> for writes — one endpoint
per action, each with its own schema. Trades REST purity for
grep-ability. For public APIs where consumers expect REST, be REST.
Error codes: keep the HTTP set small, put the “why” in the body
Most APIs I’ve owned settle on nine HTTP status codes:
200, 201, 400, 401, 403, 404, 409, 422, 429, 500.
Each of those carries meaning that infra tooling reads directly —
429 in particular you can’t bury in a body payload without breaking
every rate-limit-aware client. 409 and 422 do real work
distinguishing “conflict with current state” from “well-formed but
semantically invalid.”
Anything more granular than that goes in a numeric error code in the
response body: { error: { code: 4012, message: "..." } }. HTTP
status is the transport-layer signal — the domain-level “why” is a
business concern and belongs in the payload. Consumers switch on
numbers cleanly; they can’t switch on prose.