DEV Community

Cover image for 5 Things Even AI Can't Do, REST API
DevUnionX
DevUnionX

Posted on

5 Things Even AI Can't Do, REST API

Roy Fielding Would Like a Word About Your "REST API"

In October 2008, Roy Fielding — the person who literally invented REST in a year 2000 PhD dissertation, the same person who co-authored HTTP — opened a blog post with this sentence:

I am getting frustrated by the number of people calling any HTTP-based interface a REST API.

He then proceeded, over about a thousand words, to roast the entire industry for calling things REST that were not, in fact, REST. The line that everyone still quotes, eighteen years later:

That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.

Reader, I have built dozens of APIs in my career that I called REST APIs, that were not REST APIs by Fielding's definition. You probably have too. Almost every "REST API" you have ever used is, by the inventor's own standard, not REST. And the industry has collectively decided that's fine, and we use the word REST anyway, and Fielding is presumably still grumpy about it from his office in California.

This article is the long version of what REST actually is, what people mean when they say REST in 2026, the difference between the two, and the daily-practice details — status codes, idempotency, pagination, error formats, versioning — that you actually need to get right whether you call your API REST or REST-ish or HTTP+JSON or whatever you want to call it. Get coffee. There's a lot of ground to cover.

The Dissertation Nobody Read

Roy Thomas Fielding's PhD dissertation, Architectural Styles and the Design of Network-based Software Architectures, was published at UC Irvine in 2000. It is 180 pages long. Chapter 5 is titled "Representational State Transfer (REST)" and it's where the term came from. If you've never read it, that's fine — almost nobody has. But you should know it exists, because every argument about whether something is "really REST" eventually comes back to this document.

The setup matters. Fielding wrote the dissertation while he was simultaneously co-authoring the HTTP/1.1 specification and the URI standards at the IETF. He wasn't theorizing about the web from the outside. He was building the web while he was writing about how the web should work. That's why REST has the authority it has — the person who defined the style was also one of the people defining the protocol it runs on.

REST, per Fielding, is defined by six constraints. Five are mandatory, one is optional:

  1. Client–Server — separation of concerns. The client and the server evolve independently.
  2. Stateless — every request contains all the information needed to understand it. The server stores no client session state.
  3. Cacheable — responses must declare themselves cacheable or not.
  4. Uniform Interface — this is the big one. We'll get to it.
  5. Layered System — clients can't tell whether they're talking to the origin server or an intermediary.
  6. Code on Demand (optional) — the server can send executable code to extend the client.

That fourth one, the uniform interface, has four sub-constraints, and this is where the industry quietly stopped following the rules:

  • Identification of resources
  • Manipulation of resources through representations
  • Self-descriptive messages
  • Hypermedia as the engine of application state

That last sub-constraint has an acronym: HATEOAS. It means that a REST API's responses should contain links — hypermedia — telling the client what it can do next. Like a webpage. You don't memorize URLs to navigate a website; you click links. A real REST API works the same way: the client starts at one entry point, gets back a response with links, and follows those links to navigate the application.

Almost no API you have ever used works this way.

The 2008 Rant That Aged Like Wine

By 2008, "REST" had become an industry buzzword. Every company was launching a "REST API" — by which they meant they had endpoints that returned JSON. Fielding watched this happen for a few years and then snapped. The October 20, 2008 post titled "REST APIs must be hypertext-driven" is the foundational document of every "well actually, that's not really REST" argument that has happened on Twitter since.

Some choice cuts:

if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.

In the comments, he doubled down:

A truly RESTful API looks like hypertext.

And the line that I think about the most, also from the comments:

REST is software design on the scale of decades: every detail is intended to promote software longevity and independent evolution. Many of the constraints are directly opposed to short-term efficiency.

That last sentence is, I think, the real reason HATEOAS lost. Engineers under deadline pressure don't optimize for decades. They optimize for the next sprint. HATEOAS makes APIs more resilient over very long time horizons at the cost of immediate complexity. So we all just shipped Level 2.

The Richardson Maturity Model

A better way to think about REST in practice comes from Leonard Richardson, who presented the Richardson Maturity Model in 2008 — coincidentally the same year as Fielding's rant. Martin Fowler wrote it up on his blog and it stuck. It's the most useful diagnostic tool in the industry.

Level 0 — "The Swamp of POX." You have one URL. Everything is a POST. The request body decides what happens. This is SOAP. This is /api/endpoint taking { "action": "getUser", "id": 123 }. This is the bottom.

Level 1 — Resources. You have distinct URIs for distinct things. /users, /orders/42. You might still POST everything, but at least the URLs identify resources.

Level 2 — HTTP Verbs. You use GET for reading, POST for creating, PUT for replacing, PATCH for partial updates, DELETE for deleting. You return proper status codes — 200 for success, 404 for not found, 500 when you've broken. This is where Stripe lives. This is where GitHub lives. This is where every "REST API" you've used lives. This is what 99% of the industry calls "REST."

Level 3 — Hypermedia controls (HATEOAS). Every response embeds links telling the client what actions are possible next. The client navigates the API by following links, not by constructing URLs from memory.

Fielding's 2008 post is, basically, a protest that nothing below Level 3 should be called REST. Richardson's model is more diplomatic — it gives you a ladder, lets you see where you are, and doesn't insist you climb all the way up.

The industry stopped at Level 2 and renamed it REST. That's the whole story. Everything from here on out is just the engineering details of doing Level 2 well.

What "REST API" Actually Means In 2026

Let me give you the honest, working developer's definition. A "REST API" in 2026 is an API that:

  • Uses HTTP as its transport
  • Returns JSON (almost never XML anymore)
  • Has resource-oriented URLs — nouns, not verbs (/users/123, not /getUser?id=123)
  • Uses HTTP verbs correctly (GET reads, POST creates, etc.)
  • Returns standard HTTP status codes that aren't lying
  • Is documented, ideally with OpenAPI

That's it. That's the definition that 99% of engineers carry in their heads and 99% of working APIs satisfy. It is, strictly speaking, Level 2 on the Richardson scale. It is, strictly speaking, not REST per Fielding. Nobody cares.

The reason we still call it REST is that the alternative names are worse. "HTTP+JSON API" is accurate but clumsy. "REST-ish" is honest but apologetic. "Web API" is too broad. "RESTful" is a fig leaf — it signals "we know we're not quite REST but please give us partial credit." So we say REST, everyone knows what we mean, and we move on.

HTTP Verbs and the Idempotency Religion

Here's the verb cheat sheet you actually need:

  • GET — read a resource. Idempotent. Safe. Cacheable. Don't put side effects here. Yes, I know your tracking pixel does. Don't.
  • POST — create a resource, or "do a thing." Not idempotent.
  • PUT — replace a resource entirely. Idempotent.
  • PATCH — partially update a resource. Can be idempotent if you design it that way; often isn't.
  • DELETE — delete a resource. Idempotent.

The word "idempotent" gets thrown around like everyone agrees what it means, so let me state it cleanly: a request is idempotent if making it N times produces the same server state as making it once. Deleting the user with ID 42 ten times leaves you with the same state as deleting them once — they're gone. So DELETE is idempotent. POSTing "create a new order" ten times creates ten orders. So POST is not idempotent.

Why does this matter? Because networks are unreliable. Your client sends a request, the network drops the response, the client doesn't know if the request succeeded or not. If the request was idempotent, retrying is safe. If not, retrying might charge your customer twice.

Stripe has the canonical solution to this and it's worth knowing by heart. They use an HTTP header called Idempotency-Key. The client generates a unique key (a UUID, typically) and sends it with the request:

curl https://api.stripe.com/v1/charges \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -H "Idempotency-Key: AGJ6FJMkGQIpHUTX" \
  -d amount=2000 -d currency=usd
Enter fullscreen mode Exit fullscreen mode

The server stores the result of the first request under that key for a window (24 hours for Stripe v1, 30 days for v2). If the same key comes in again, the server returns the cached result instead of charging the customer again. The client can retry as many times as it wants without fear of double-charging.

Brandur Leach's "Designing robust and predictable APIs with idempotency" on the Stripe engineering blog is required reading if you're building anything that touches money. The pattern works for any non-idempotent operation, not just payments.

Status Codes That Actually Matter

There are sixty-something HTTP status codes. You need to know about fifteen of them. Here's the working set:

200 OK — Success, with a body. The default for GET, PUT, and PATCH responses.

201 Created — A new resource was created. Should include a Location header pointing to the new resource. Useful for POSTs that create something.

204 No Content — Success, intentionally empty body. The spec says you cannot send a body with a 204. The natural answer for DELETE and for PUTs that don't need to echo back the updated resource. People argue endlessly about whether DELETE should return 200 (with the deleted entity in the body) or 204 (with nothing). Both are defensible. Pick one and be consistent.

301 Moved Permanently — Useful for URL versioning migrations. The browser caches this aggressively, so be careful.

400 Bad Request — The client sent garbage. Malformed JSON, missing required fields, that sort of thing.

401 Unauthorized — You aren't authenticated. The spec calls this "Unauthorized" but it really means "Unauthenticated." Everyone gets this wrong because the name is bad.

403 Forbidden — You ARE authenticated, but you don't have permission to do this thing. The "you can't sit with us" of HTTP.

404 Not Found — The resource doesn't exist. GitHub famously returns 404 for private repositories you don't have access to, instead of 403, to avoid leaking information about what exists. That's a defensible third path between 403 and 404.

409 Conflict — State conflict. Duplicate keys, version mismatches, "this email is already taken."

422 Unprocessable Content — Your JSON parsed fine, but it's semantically wrong. Validation failures, basically. This started as a WebDAV-specific code and got repurposed by the industry because there was a gap between "syntactically broken" (400) and "logically wrong" (which previously had no good code).

429 Too Many Requests — Rate limited. Defined by RFC 6585 in April 2012. You should include a Retry-After header telling the client how long to wait.

500 Internal Server Error — We broke.

502 Bad Gateway — Our upstream broke.

503 Service Unavailable — We're down or overloaded. Include Retry-After if you know when you'll be back.

The 401 vs 403 confusion is something I've watched senior engineers get wrong, repeatedly, on production systems. The mnemonic: 401 means "who are you," 403 means "I know who you are, no."

URL Design: The Trailing Slash and Other Religious Debates

Resource-oriented URLs use nouns, not verbs. GET /users/123/posts is good. GET /getUserPosts?id=123 is bad. This is one of the few things the entire industry agrees on.

Use plurals for collections. /users, not /user. The Microsoft API guidelines, Google's API guidelines, Zalando's guidelines, and basically everyone else agree. Yes, "/people" is more grammatically correct than "/persons," but consistency beats correctness — pick the rule "always plural" and apply it uniformly.

For nested resources, prefer two-level nesting at most. /users/123/posts is fine. /users/123/posts/456/comments/789/reactions/abc is hardcoding your data model into your URLs in a way you will regret. The pragmatic alternative: /comments/789/reactions or /reactions?comment_id=789.

Now the trailing slash. /users versus /users/. People have died on this hill. John Sheehan of Runscope said it best, and the API Evangelist quoted him: save the byte, drop the slash, and 301-redirect from the slashed version to the non-slashed version if anyone hits it. That's the pragmatic answer. By the strict reading of Fielding's dissertation, URIs are character-by-character identifiers, which means /users and /users/ are different resources unless the server normalizes them. In practice, your framework probably normalizes them and nobody on your team will ever notice.

Versioning Is A Mess

There is no good answer here. There are three answers, all bad in different ways.

URL versioning (/v1/users, /v2/users) is what Stripe does, what most pragmatic teams do, and what I'd recommend for new APIs. It's discoverable. It's debuggable in a browser tab. It's obvious in logs. The downside is that the URL of a resource is supposed to be the identity of the resource, and /v1/users/42 and /v2/users/42 are arguably the same user with different representations, which is what content negotiation is for. But almost nobody cares about this in practice.

Header versioning (X-API-Version: 2) keeps URLs clean and lets you handle versions via middleware. GitHub does this with calendar-based versioning — their header is X-GitHub-Api-Version: 2022-11-28, and in March 2026 they released their first calendar version with breaking changes, 2026-03-10. Stripe also uses dated versions, like 2025-12-15.clover. The calendar-based approach has the nice property that "v2" doesn't have to mean "we changed everything" — each release is a small, dated diff from the previous.

Content negotiation (Accept: application/vnd.myapi.v2+json) is the "by the book" REST answer. Almost nobody uses it because debugging an API in a browser tab is impossible when you have to set custom Accept headers. It exists. You can ignore it.

If I'm building a new API in 2026 and I have no constraints, I pick calendar-based versioning in a header, like GitHub. If I'm building something simpler and I want minimum friction, I pick /v1/ in the URL and live with it.

Pagination Has No Standard

There is no RFC for pagination. There is no IETF spec. There is no industry agreement. Every API does it differently. This is genuinely embarrassing and I don't expect it to be fixed in my career.

The four approaches in the wild:

Offset/limit (?offset=20&limit=10). Simple. You can jump to page 47. But if data shifts under you — someone inserts or deletes a row — you'll see duplicates or skips. Performance falls off a cliff at high offsets because the database has to count past all the previous rows.

Cursor-based (?cursor=eyJpZCI6MTAwfQ&limit=10). The cursor is an opaque base64-encoded token that the server understands. Stable under inserts. You can't jump to an arbitrary page, but that's usually fine — infinite scroll doesn't need page jumping. Stripe uses this. Slack uses this. GitHub uses this for several endpoints. This is the right answer for most APIs.

Keyset (?after_id=100). A non-opaque cursor based on an indexed column. Slightly less flexible than opaque cursors but easier to debug.

Page-based (?page=3&per_page=10). Cosmetic offset/limit. Same problems.

Zalando's API guidelines recommend cursors for anything more than a few hundred items. I agree. If your collection might exceed a thousand items, use cursors. If it's always small (a user's recent orders, say), offset/limit is fine.

There's a beautiful idea, almost never implemented in practice, of putting pagination links in HTTP headers via RFC 8288 (the Web Linking spec):

Link: <https://api.example.com/items?page=3>; rel="next",
      <https://api.example.com/items?page=10>; rel="last"
Enter fullscreen mode Exit fullscreen mode

GitHub has done this since their V3 days. It's elegant — the URLs are right there, the client doesn't have to construct them. Nobody else does it because most clients don't parse Link headers automatically and most developers don't think to look.

Error Responses: RFC 7807 Exists And Almost Nobody Uses It

In March 2016, Mark Nottingham (of Akamai, also known for the HTTP cache header Cache-Control) and Erik Wilde published RFC 7807, "Problem Details for HTTP APIs." In July 2023 it was updated as RFC 9457. The media type is application/problem+json. Here's what an RFC-7807-compliant error response looks like:

HTTP/1.1 404 Not Found
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/user-not-found",
  "title": "User Not Found",
  "status": 404,
  "detail": "User with ID 12345 does not exist",
  "instance": "/users/12345"
}
Enter fullscreen mode Exit fullscreen mode

Five fields. type is a URI identifying the kind of problem (which the client can recognize and handle programmatically). title is a short human-readable name. status echoes the HTTP status. detail is a longer human-readable explanation. instance is the URI of this specific occurrence.

It's elegant. It's standardized. It's been around for almost a decade. ASP.NET Core ships it by default. Spring Boot has built-in support. And almost nobody outside those two ecosystems uses it. Instead, every API invents its own error format:

// Common
{ "error": "User not found" }

// Also common
{ "errors": [{ "code": "USER_NOT_FOUND", "message": "..." }] }

// Why did anyone do this
{ "success": false, "data": null, "error": { "msg": "..." } }

// The cardinal sin  200 OK with this body
{ "ok": false, "error": "Something went wrong" }
Enter fullscreen mode Exit fullscreen mode

That last one — returning HTTP 200 with an error in the body — is the worst pattern in REST and Facebook's Graph API did it for years. Every middleware, every load balancer, every monitoring tool understands HTTP status codes. When you return 200 for an error, you've thrown away your ability to alert on errors, retry intelligently, or cache responses. You've made every client write a custom error handler that ignores the status code and reads the body. Please don't.

If you're building a new API, use RFC 7807 / 9457. The format is good. The standard exists. You don't have to invent error handling from scratch.

Authentication: The Real Cheat Sheet

You have basically five options. Pick the right one for your situation:

API keys are the simplest thing that works. Send Authorization: Bearer sk_live_abc123 in the header. Stripe does this. It works. The downside is that the key has all-or-nothing access (no scopes), and if it leaks, you have a problem. Don't put keys in URLs — they end up in server logs, browser history, and analytics tools. Header only.

Basic Auth (Authorization: Basic <base64(user:pass)>) is older than the web you grew up with. It's still everywhere, especially for internal services and webhook signing. It's fine. It's not great. Use HTTPS or die.

OAuth 2.0 is the right answer for any API that third parties will integrate with. The flows that actually matter in 2026:

  • Authorization Code with PKCE — the default for SPAs and mobile apps. PKCE (Proof Key for Code Exchange, RFC 7636) is now effectively mandatory under the OAuth 2.1 draft and RFC 9700's security best practices. Even Anthropic's Model Context Protocol adopted OAuth 2.1 for tool authorization.
  • Client Credentials — for server-to-server, where there's no user.
  • Refresh Token Rotation — for keeping SPAs logged in despite browser cookie restrictions.

JWT bearer tokens are a love/hate relationship. They're self-contained (no DB lookup needed to validate), but you can't revoke them without building a denylist, which defeats the point of having them be self-contained. The right framing: JWTs are great for short-lived access tokens (15 minutes), terrible as long-lived session tokens. If your JWT lives for 30 days, you've reinvented session cookies, badly.

Session cookies are still completely valid in 2026, often the right answer for first-party web apps, and unjustly maligned by everyone who reaches for JWTs out of habit. They have a security story (HttpOnly, Secure, SameSite) that's been honed for thirty years. Use them when they fit.

PUT vs PATCH: The Confusing One

PUT replaces the resource entirely. If the resource was { name: "Jane", email: "j@example.com", role: "admin" } and you PUT { name: "Jane Doe" }, the resource is now { name: "Jane Doe" } — the email and role are gone, because PUT replaces.

PATCH applies a partial update. If you PATCH { name: "Jane Doe" } to the same resource, the result is { name: "Jane Doe", email: "j@example.com", role: "admin" } — only the name changed.

That's the conceptual difference. The complication is that there are two competing formats for PATCH, and APIs that don't tell you which one they use will make you cry.

JSON Patch (RFC 6902), media type application/json-patch+json, is an array of operations:

[
  { "op": "replace", "path": "/email", "value": "new@example.com" },
  { "op": "remove",  "path": "/nickname" }
]
Enter fullscreen mode Exit fullscreen mode

It's atomic, expressive, and lets you address individual array indices. It's also unreadable. Nobody writes these by hand.

JSON Merge Patch (RFC 7396), media type application/merge-patch+json, looks like the object you want, with null meaning "delete":

{
  "email": "new@example.com",
  "nickname": null
}
Enter fullscreen mode Exit fullscreen mode

It's intuitive. You can write it by hand. But you cannot use it to set a field to literal null, and you cannot update individual array indices — you can only replace the whole array.

GitHub uses Merge Patch–style. Kubernetes supports three different patch types — JSON Patch, Merge Patch, and a custom Strategic Merge Patch — because of course Kubernetes does. If you're building a new API and don't have strong opinions, use Merge Patch. It's simpler, and the limitations rarely matter in practice.

Rate Limiting and Caching

Rate limiting in HTTP is 429 Too Many Requests plus the Retry-After header. The RFC 6585 definition is straightforward, but the headers used to communicate quota state are not standardized. The convention everyone uses (without an RFC) is:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1714232400
Enter fullscreen mode Exit fullscreen mode

There's an IETF draft (draft-ietf-httpapi-ratelimit-headers) that proposes the same headers without the X- prefix, but it's been a draft for years. If you implement rate limiting, set both Retry-After (which is standardized) and the X-RateLimit headers (which are conventional). Your clients will thank you.

The right retry strategy on the client side is exponential backoff with jitter: delay = min(cap, base * 2^attempt) + random(0, jitter). The jitter matters more than you'd think — without it, all your clients retry at exactly the same moment after a 429 storm, and you get a thundering herd.

Caching is Cache-Control, ETag, Last-Modified, and the conditional request headers (If-None-Match, If-Modified-Since). The honest truth is that almost nobody uses HTTP caching properly for JSON APIs. The reason is that in a modern stack, you have a CDN in front (handling its own caching), an application cache in Redis (handling server-side), and a client-side cache in the browser (React Query, SWR, Apollo, TanStack Query). HTTP-level caching feels like a fourth layer that doesn't pay off, and most APIs ship Cache-Control: no-store for everything authenticated.

It's a missed opportunity but I understand why it happened.

The Competing Paradigms (Brief Tour)

REST isn't the only game in town. Quick survey of the alternatives, why they exist, and why REST is still the default:

GraphQL (Facebook, 2015) solves over-fetching and under-fetching. You ask for exactly the fields you want, you get exactly those fields. Great for complex frontends with diverse data needs. Terrible operational story: caching is hard, query complexity attacks are a real concern, N+1 resolver storms unless you use DataLoader, and the bundle weight of Apollo Client is non-trivial. Use it when your client teams genuinely need flexible queries. Skip it when you're just doing CRUD.

gRPC (Google, 2015) is Protocol Buffers over HTTP/2. Binary, fast, schema-first, with code generation for every major language. The current stable version as of early 2026 is gRPC 1.80.0 (released March 30, 2026). Benchmarks vary but you can expect roughly an order of magnitude smaller payloads than equivalent JSON, and meaningful latency wins for chatty internal services. The catch: browsers can't speak it natively, so for browser-facing APIs you need grpc-web plus a proxy. Use it for east-west service-to-service communication, especially at scale. Don't bother for public APIs.

tRPC is TypeScript-only RPC where your TypeScript types are the contract. No schema, no codegen, no OpenAPI. Tiny bundle (around 5KB). If you're shipping a TypeScript monorepo with a Next.js frontend and a Node backend, it eliminates a whole layer of contract management. Useless outside the TypeScript ecosystem.

Server-Sent Events (SSE) is one-way streaming from server to client over plain HTTP. The browser supports it natively through EventSource. This is what OpenAI and Anthropic use to stream AI tokens. It's underrated. If you need streaming and you don't need bidirectional, use SSE before reaching for WebSockets.

WebSockets are full-duplex, separate protocol after an HTTP upgrade. For chat, multiplayer games, real-time collaboration. Overkill for most uses.

AsyncAPI is the specification for documenting event-driven APIs (Kafka, MQTT, AMQP, WebSockets, etc.) — the equivalent of OpenAPI but for messages instead of requests. Current version is 3.1.0, released January 31, 2026.

Why is REST still the default in 2026 despite all of this? Because it's the lowest common denominator. Every language has an HTTP client. Every developer can curl your API. Every API gateway, CDN, monitoring tool, and WAF understands HTTP+JSON for free. The cost of "more correct" alternatives is real and often not worth paying for typical CRUD workloads.

The 2026 Angle: AI Agents Read Your API

Here's the new thing in 2026 that wasn't a thing five years ago: LLM agents consume APIs. They read the OpenAPI spec, they figure out the tools available, they make the calls themselves. Google's Agent Development Kit has OpenAPIToolset. The FastMCP library has FastMCP.from_openapi(). Anthropic's Model Context Protocol standardizes the tool-discovery layer that bridges LLMs and APIs.

OpenAPI 3.2.0 (dated September 19, 2025) added native streaming media types — SSE, JSON Lines, multipart feeds — exactly because AI workloads need them. The OpenAPI Initiative has explicitly oriented the spec toward AI consumption.

What this means for API designers: your API documentation is now an LLM-readability concern. Vague operation IDs, missing descriptions, chatty endpoints with poor schemas — these used to be human-only problems. Now they cause AI agents to hallucinate, misuse your API, or fail to use it at all. Good operation names (createUser, not endpoint_v2_user_create_new), thorough descriptions, well-typed request and response schemas, and clear error responses are no longer nice-to-have. They are the difference between an LLM agent that can use your API and one that can't.

If you maintain a public API, generating high-quality OpenAPI 3.2 specs is probably the highest-leverage investment you can make in 2026. The audience for your docs now includes machines.

The Mistakes I Have Watched Get Made In Production

Permit me a personal list. Every one of these I have either shipped myself or watched a colleague ship:

POST for everything. I have seen POST /api/getUser in production. At three different unicorns. Yes, in production. There is no excuse. Use GET for reads.

HTTP 200 with { "success": false, "error": "..." }. Throws away every benefit of HTTP status codes. Breaks monitoring, breaks caching, breaks retries.

Inconsistent casing in the same response. { "userId": 1, "user_name": "alice" }. This happens. Usually because the field was added by a different team that hadn't read the style guide.

Verbs in URLs. /getUser, /createOrder, /deleteAccount. Verbs go in the HTTP method. That is literally what HTTP methods are for. If your URL has /get or /create or /delete in it, you're doing RPC and you should at least be honest about it.

No pagination on collection endpoints. Until one customer has 50,000 invoices, and then your API times out, and then you spend a sprint adding pagination retrofit. Add pagination from day one, even if you don't think you'll need it.

Ignoring idempotency for create operations. Network blip, double-charged customer, angry email at 2am. Use Stripe's pattern. Implement idempotency keys.

Massive nested URLs. /users/:id/orders/:orderId/items/:itemId/notes/:noteId. You've now hardcoded your data model into your URL structure. When the data model changes, every URL is a breaking change. Flatten where possible.

Not using HTTP cache headers, even when you could. Public, read-heavy endpoints can absolutely use proper Cache-Control and ETag responses. The CDN will love you. Your origin will love you. Your bill will love you.

Versioning policy of "we'll figure it out later." Then later arrives, you have ten thousand customers depending on v1, and you can't change anything without breaking the world. Adopt calendar versioning from day one, or commit to URL versioning with a clear deprecation policy. Either is fine. Both beat nothing.

The Two Reference APIs You Should Read

If you want to learn what good REST API design looks like, there are two public APIs to study.

Stripe is the canonical "good REST API." Their own docs put it plainly: the Stripe API is organized around REST, with predictable resource-oriented URLs, form-encoded bodies, JSON responses, standard HTTP status codes, and standard HTTP authentication. The little details matter — typed prefixes for object IDs (cus_ for customers, pi_ for payment intents, in_ for invoices), the Idempotency-Key header pattern, the expand[] query parameter for embedding related resources (avoiding N+1 round trips), the dated version strings. Read the docs. Steal liberally.

GitHub is the canonical "evolved over a decade and shows it." The API has been alive since around 2008. It's been through multiple versioning schemes, multiple authentication paradigms, multiple pagination styles. It's a museum of every fashion in REST API design since the late 2000s, all coexisting in one product, none of them quite consistent with each other. Read it as a cautionary tale about what happens when you don't decide on conventions early and stick to them.

Stripe and GitHub between them will teach you 90% of what you need to know about REST API design in practice. Read them. Compare them. Notice what Stripe got right that GitHub didn't. Then go design your own API with that knowledge.

Tooling You Should Actually Be Using

The 2026 tooling landscape:

For API documentation: OpenAPI 3.2.0. Generate it from your code (FastAPI, tRPC's OpenAPI plugin, NestJS, Spring) or write it by hand and use it to generate code (openapi-generator, Stainless, Speakeasy, Fern, Kiota). Either direction works. Pick one.

For API exploration: Postman is still #1 but increasingly hated for cloud-sync requirements and pricing. Insomnia got acquired by Kong and lost mindshare. Bruno is the breakout 2024 alternative — open-source, MIT-licensed, stores collections as plain-text files on disk for proper Git workflows, no cloud, no login, no telemetry. As of early 2026 it has around 41,000 GitHub stars and is what most developers I know have switched to. Hoppscotch is the lightweight browser-first option.

For API mocking: MSW (Mock Service Worker) for frontend tests. Prism for mock servers from OpenAPI specs.

For API linting: Spectral, from Stoplight. Catches inconsistencies, enforces house style. Put it in CI.

For the command line: curl for everything serious. HTTPie if you want commands to read like English (http POST api.example.com/users name=alice).

That's it. Five tools cover 99% of API workflows. Don't overthink your tooling.

The Closing Argument

I've written this article in a slightly grumpy mood because I think the gap between Fielding's REST and what we call REST is one of the most interesting stories in modern software, and most articles on the topic skip it entirely. The honest version is that we — the industry — borrowed a term, simplified it, and built something useful but not what the inventor meant. Whether that's fine or a sin against software longevity depends on how much you take Fielding's "decades-scale design" seriously.

Personally, I think it's fine. HATEOAS was a beautiful idea that didn't survive contact with the average engineering team's quarterly priorities. The Level 2 industry standard — HTTP+JSON with proper verbs and status codes — has shipped countless functional systems that work well enough for their lifespans. The cost of doing real REST was higher than the benefit for most teams. So we did the cheaper thing and called it REST anyway. The web didn't end.

But the details still matter. Use the right HTTP verb. Return honest status codes. Don't return 200 with an error in the body. Use cursor-based pagination for large collections. Implement RFC 7807 problem details for errors. Pick a versioning strategy and commit to it. Add idempotency keys for non-idempotent writes. Generate good OpenAPI specs because the LLMs are reading them now too. These are the things that separate an API your customers love from one they have to write a wrapper library around.

Roy Fielding probably still doesn't think any of this is REST. But if we get the daily details right, our APIs are going to outlive us anyway, which was the point.

Now go fix the one in your repo that returns 200 with { "success": false }. You know which one.

Top comments (0)