REST API Design
How I design APIs that survive their second consumer: resource modeling, versioning, error contracts, and the habits from migrating PHP and GraphQL systems to Java REST at marketplace scale.
2 min read
An API is a promise you make to people you haven't met yet. Most API pain I've debugged, on a marketplace serving millions, on an insurance platform, and on my own products, traces back to a promise that was never written down: an error shape that changed between endpoints, a field that meant two things, a "temporary" endpoint that lived for six years.
When I migrated a marketplace's PHP and GraphQL projects to Java REST APIs (cutting latency around 30% along the way), the performance win came from the runtime. The maintainability win came from being strict about a few contracts.
Resources, not verbs
The URL space is your domain model in public. I keep it boring and predictable:
GET /v1/orders # list (filterable, paginated)
POST /v1/orders # create
GET /v1/orders/{orderId} # read
PATCH /v1/orders/{orderId} # partial update
POST /v1/orders/{orderId}/refunds # sub-resource for actions with a nounThe last line is the important habit. When someone asks for an "action endpoint" (/cancelOrder), there's almost always a noun hiding in it (a cancellation, a refund) that deserves to be a resource, because tomorrow someone will ask to list them, audit them, or undo them.
One error contract, everywhere
Consumers write their error handling against the first error they see, so every endpoint must fail the same way. I use a single envelope, aligned with RFC 9457 (Problem Details):
{
"type": "https://api.example.com/errors/insufficient-stock",
"title": "Insufficient stock",
"status": 409,
"detail": "Requested 5 units of SKU 8891; 2 available.",
"traceId": "b7ad6b7169203331"
}The traceId is non-negotiable. On the insurance platform, correlating a customer complaint to a log line across microservices was the difference between a five-minute answer and a half-day hunt.
Versioning is a social problem
I version in the path (/v1/) not because it's elegant (it isn't) but because it's visible. Header-based versioning is cleaner in theory and invisible in practice: invisible in logs, invisible in browser devtools, invisible to the intern reading a curl example. Additive changes (new optional fields) don't need a version. Changed meanings always do.
Design for the second consumer
The first consumer of your API works with you and forgives you. The second one reads your OpenAPI spec at 2am in another timezone. Everything I ship has a spec that is generated from code or verified in CI. A spec that can drift is worse than no spec, because it's confidently wrong.
Next topic
System Design: The BFF Pattern