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 caseBest shape
CRUD over entities (users, products)REST
Workflow with named transitionsAction-oriented
Service-to-service typedRPC / gRPC
Public API consumed by anonymous clientsREST
Internal API in TypeScript fullstacktRPC
Browser → backend with shared typesConnect

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: /users not /user.
  • Use kebab-case in URLs: /order-items not /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

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 .