There are three shapes of API. Most teams pick one religiously when each shines for different problems. This post is the practical guide.
The three shapes
Resource-oriented (REST)
GET /api/orders
GET /api/orders/42
POST /api/orders
PATCH /api/orders/42
DELETE /api/orders/42
GET /api/orders/42/items
Nouns and verbs. Maps cleanly to CRUD on entities. HTTP semantics give you caching, status codes, idempotency for free.
Best for: entity-shaped data with consistent CRUD operations.
Action-oriented
POST /api/orders
POST /api/orders/42/cancel
POST /api/orders/42/refund
POST /api/orders/42/dispatch
POST /api/orders/42/mark-paid
Custom verbs as endpoints. Maps cleanly to commands that aren’t a simple update.
Best for: workflow-rich APIs where actions don’t fit “PATCH this field.”
RPC-style
POST /api/createOrder
POST /api/cancelOrder
POST /api/getOrderById
POST /api/listOrdersByCustomer
Function calls over HTTP. Often paired with typed contracts (gRPC , tRPC ).
Best for: service-to-service, typed clients, code-gen.
When each wins
| Use case | Best shape |
|---|---|
| CRUD over entities (users, products) | REST |
| Workflow with named transitions | Action-oriented |
| Service-to-service typed | RPC / gRPC |
| Public API consumed by anonymous clients | REST |
| Internal API in TypeScript fullstack | tRPC |
| Browser → backend with shared types | Connect |
For the deeper RPC comparison see GraphQL vs tRPC vs gRPC vs REST .
Hybrids that work
Most production APIs mix:
- REST for resources, action-oriented for transitions:
POST /orders ← create
GET /orders/42 ← read
PATCH /orders/42 ← edit fields
POST /orders/42/cancel ← named action
POST /orders/42/refund ← named action
This is the GCP API style — RESTful resources with custom verbs for actions. Reads cleanly; everyone understands it.
- REST for browser, gRPC service-to-service: REST at the edge, gRPC inside. Standard pattern.
Readability matters
The API you pick is read 100× more than written. Optimize for the reader:
- Use plural nouns:
/usersnot/user. - Use kebab-case in URLs:
/order-itemsnot/orderItems. - Be consistent across endpoints. Mixed conventions are friction.
- Document with OpenAPI; auto-generate clients.
Status codes that matter
200 OK - success with body
201 Created - resource created (Location header)
202 Accepted - async; check back later
204 No Content - success, no body
400 Bad Request - malformed input
401 Unauthorized - need to authenticate
403 Forbidden - authenticated but not allowed
404 Not Found - resource doesn't exist
409 Conflict - state conflict (idempotency, version)
422 Unprocessable - validation failed
429 Too Many - rate limited (Retry-After)
500 Internal - server bug
503 Service Unavailable - down or overloaded
Use them. Don’t return 200 with {"error": "not found"}.
Versioning
Three styles:
- URL path:
/v1/users. Clearest; easy to grep. - Header:
Accept: application/vnd.example.v1+json. Cleanest URLs. - Query string:
/users?api_version=1. Avoid; messes with caching.
Pick URL path unless you have a reason. Versions in URLs survive forever.
Pagination
Cursor-based, not offset:
{
"items": [...],
"next_cursor": "eyJpZCI6MTAwfQ=="
}
Offset breaks under inserts at the head; cursors don’t.
Idempotency
For non-GET requests:
POST /payments
Idempotency-Key: <client-generated-uuid>
See Idempotency, Retries, and Exactly-Once Illusions .
Read this next
- Designing REST APIs That Don’t Suck
- GraphQL vs tRPC vs gRPC vs REST
- FastAPI + Pydantic v2 + SQLAlchemy 2.0
- Modern TypeScript Backend with Hono on Bun
If you want my API style guide template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .