C.W.K.
Stream
Lesson 01 of 07 · published

Resources vs RPC — Two Mental Models, Both Valid

~11 min · rest-design, resources-vs-rpc, mental-model

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Most 'REST APIs' shipping today are RPC with HTTP costumes. That's not an insult — it's a useful observation. The question isn't 'which is correct' but 'which mental model are you actually using and is it serving you?'"

Two Design Centers

When you sit down to design an API, the first decision shapes everything that follows: what is the API organized around?

Resource-oriented (REST proper). The world is made of things — users, orders, payments, conversations. Each thing lives at a URI. The seven HTTP methods act on them. Reading is GET; creating is POST to a collection; replacing is PUT; deleting is DELETE. The verbs are pre-defined; you only design nouns.

RPC (Remote Procedure Call). The world is made of operations — createUser, processPayment, finalizeCouncil. Each operation has a procedure name and arguments. You POST a payload to invoke it. The nouns disappear into the procedure names; the verbs are unbounded (you invent new ones as needed).

Both work. Both ship in production. The decision is which mental model fits the domain and your team's habits.

The Litmus Test — Count Your URIs

Look at any API's URI list. If most URIs name things (/users/42, /orders/abc, /payments/xyz), it's resource-oriented. If most URIs name operations (/createUser, /processOrder, /getPaymentStatus), it's RPC.

A quick survey of 2026's well-known APIs:

  • Resource-leaning: Stripe (mostly), GitHub REST, AWS S3, Kubernetes API. URIs name things.
  • RPC-leaning: Slack Web API (chat.postMessage, users.list), most gRPC services, OpenAI Chat Completions (POST /v1/chat/completions). URIs name operations.
  • Hybrid: Most real APIs. Resource URIs for the obvious nouns, plus action endpoints for operations that don't fit (POST /payments/{id}/refund, POST /jobs/{id}/cancel). cwkPippa is in this bucket.

When Each Model Shines

Resource-oriented wins when:

  • The domain has clear nouns. CRUD-heavy applications (admin panels, content management, e-commerce catalogs) map cleanly.
  • Cache friendliness matters. GET-driven access is what CDNs were built for.
  • Clients are diverse (browsers, mobile, third-party integrations). The seven verbs are universally understood; learning a new noun is easier than learning a new procedure.
  • You want to expose discoverability and convention. /users/{id}/orders is self-explanatory; getOrdersForUser(userId) requires reading docs.

RPC-leaning wins when:

  • The domain is operation-heavy. AI chat ("complete this prompt"), workflow orchestration, transcoding, ML inference — all naturally read as "do X with these args."
  • You control both client and server tightly. gRPC/Protobuf is the canonical example: typed contracts, generated stubs, code-first.
  • The operations don't naturally map to nouns. "Send a notification to these users on these channels with this severity" is awkward as a resource; clean as a procedure.
Coherence beats purity. A resource-oriented API with three well-justified action endpoints is fine. A 'pure REST' API that tortures every operation into a resource shape becomes a riddle. Pick a design center; deviate only when the alternative is worse than the deviation.

The Hybrid in Practice

Most production APIs land on resource-oriented with action sub-resources for awkward operations. The pattern: nouns at the top level (/orders, /payments), actions as sub-paths under a specific resource (POST /orders/42/cancel, POST /payments/xyz/refund). The action is named, the verb is POST (because actions are typically not idempotent without coordination), and the target is the parent resource.

Why this works: discovery is preserved (you find /orders/42 and see what you can do to it), the resource-oriented mental model stays intact, and the operations that genuinely don't fit ("refund" isn't a noun, it's a thing-you-do-to-a-payment) get a clean home.

cwkPippa's Mix

cwkPippa is hybrid. Most of backend/routes/ is resource-oriented: /api/conversations, /api/folders, /api/messages, /api/artifacts all act like resources with GET/POST/PUT/PATCH/DELETE. But POST /api/council/{id}/finalize, POST /api/council/{id}/inject, POST /api/heartbeat/cron/{id}/run-now are action endpoints — they don't fit the resource model because they're operations on existing resources, not creates. The mix is deliberate; trying to force council finalization into a 'finalizations' resource would be ceremony for its own sake.

Code

Resource-oriented with hybrid action endpoints·text
# Resource-oriented design — count the nouns
GET    /users                      # list resources
POST   /users                      # create resource (server assigns ID)
GET    /users/{id}                 # read one resource
PUT    /users/{id}                 # replace
PATCH  /users/{id}                 # partial update
DELETE /users/{id}                 # remove

GET    /users/{id}/orders          # nested resource — orders belonging to a user
POST   /users/{id}/orders          # create order under user

# Action endpoints (the practical hybrid escape hatch)
POST   /orders/{id}/cancel         # action — POST + named sub-path
POST   /payments/{id}/refund       # action — same pattern
RPC-style: every URI is a verb, every method is POST·text
# Pure RPC design — count the verbs
POST   /users.list                 # list users (Slack-style)
POST   /users.create               # create user
POST   /users.get                  # read one
POST   /users.update               # update
POST   /users.delete               # delete
POST   /orders.cancel              # action
POST   /payments.refund            # action

# Notice: every URI is an operation, every method is POST.
# Status codes still work, but the protocol's seven verbs collapse into one.
The same logic, two design centers — pick the costs you prefer·python
# Same business logic, two ways. Both work; tradeoffs differ.
from fastapi import FastAPI

app = FastAPI()

# ============= Resource-oriented =============
@app.get('/orders/{oid}')
async def get_order_resource(oid: str):
    return await load_order(oid)

@app.post('/orders/{oid}/cancel', status_code=202)
async def cancel_order_resource(oid: str):
    return await cancel(oid)

# ============= RPC-style ====================
@app.post('/orders.get')
async def get_order_rpc(payload: dict):
    return await load_order(payload['order_id'])

@app.post('/orders.cancel')
async def cancel_order_rpc(payload: dict):
    return await cancel(payload['order_id'])

# Resource-oriented gets you cacheable GET, conditional requests, optimistic locking
# for free. RPC gets you simpler contract design and easier code generation. Pick
# the one whose costs you'd rather pay.

External links

Exercise

Pick any API you've used (Stripe, GitHub, Slack, OpenAI, cwkPippa). Look at 10 of its URIs. For each one, classify whether the URI names a THING (resource-oriented) or an OPERATION (RPC). Calculate the ratio. Is the API mostly resource-oriented, mostly RPC, or hybrid? Bonus: pick one operation the API currently does RPC-style and redesign it resource-oriented (or vice versa). What did you gain? What did you give up?
Hint
Slack's URIs look like users.list, chat.postMessage — operations. RPC. GitHub's look like /repos/{owner}/{repo}/issues — things. Resource. OpenAI's /v1/chat/completions is operation-named — RPC. cwkPippa is hybrid. The redesign exercise teaches you what each model is implicitly costing or paying. There's no right answer; the practice is in articulating the tradeoff.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.