The first API I ever shipped in a production environment had exactly one endpoint. It returned a JSON blob the size of a small novel — user data, preferences, activity history, nested arrays of nested arrays — because the frontend engineer and I sat three desks apart and it was easier to overfetch than to talk. That endpoint lived for four years. I watched it slow, bloat, and finally collapse under traffic that, in hindsight, was not even particularly impressive. The autopsy was humbling: the problem wasn't the database, wasn't the server, wasn't the team that inherited it. The problem was the shape of the thing from day one.
API design is one of those disciplines that feels trivial until it isn't. You make a few decisions in an afternoon — how to name a route, what to put in a response body, whether to version or not — and those decisions calcify into contracts that outlast the team that made them. The patterns you choose in week one are the load-bearing walls of everything built on top. And most of us, myself included at various points, choose them by instinct or imitation rather than by principle.
So what does it actually mean to design an API that scales? Not just under traffic — though that matters — but under time, under team growth, under the slow entropy of changing requirements.
The Contract You Don't Know You're Signing
Every API is a promise. The moment a consumer — internal or external — writes code against your endpoint, you've entered into an implicit agreement: this will keep working. The tragedy of most early API decisions is that they're made before the designer understands the weight of that promise.
Versioning is the clearest example. Teams routinely ship v1 of an API with the intention of versioning properly later. Later rarely comes with the same urgency. What comes instead is a v2 that exists alongside v1 indefinitely, because the three clients on v1 never migrated, because migration was never prioritized, because the API team is now four people supporting twelve internal services and two external partners. I've seen v1 endpoints with comment headers that read // remove after Q3 migration from a Q3 that passed in 2019.
The API design patterns that scale treat versioning as a first-class concern from the start. URI versioning — /v1/resource — is blunt but honest; it makes the version visible to every developer who reads a URL. Header-based versioning is cleaner in theory but invisible in practice, which is a tradeoff worth naming explicitly. The choice matters less than the commitment: version deliberately, deprecate on a published schedule, and build the assumption of change into your architecture before change arrives uninvited.
Pagination, Filtering, and the Temptation to Return Everything
That first endpoint of mine returned everything because returning everything felt generous. It felt like good service. It was neither.
Unbounded responses are one of the most reliable ways to build an API that fails at scale. They work fine in development, where the dataset is small and the latency is invisible. They work fine in staging. They fail in production at the precise moment your traffic spikes, because a response that takes 200ms to serialize at a thousand records takes twenty seconds at a hundred thousand, and by then the timeout has already fired and the client is retrying, which is making things considerably worse.
Cursor-based pagination is, in my experience, the pattern that teams resist the longest and regret skipping the most. Offset pagination — ?page=2&limit=50 — is intuitive and easy to implement, and it breaks in subtle ways when the underlying data is changing: records shift between pages, items get skipped or duplicated, and the client never knows. Cursor-based pagination anchors each page to a specific record, making it stable under mutation. It's harder to explain to a new developer. It's worth explaining.
Filtering and field selection follow the same logic. An API that lets consumers ask for exactly what they need — specific fields, specific date ranges, specific relationship depths — is an API that stops punishing consumers for the sins of other consumers. GraphQL formalized this idea into a query language; REST APIs can approximate it with thoughtful query parameter design without adopting the full GraphQL surface area. The point isn't the technology. The point is that the consumer's data needs should drive the response shape, not the convenience of the implementer.
Idempotency Is Not a Nice-to-Have
Networks lie. They drop packets, time out, return errors that aren't really errors. A client that sends a request and receives no response has no way of knowing whether the request succeeded, failed, or is still in flight. If it retries — and it will retry, because every reasonable client retries — the API needs to be ready for that.
Idempotency is the guarantee that sending the same request twice produces the same result as sending it once. For read operations, this is trivially true. For writes, it requires deliberate design. The pattern that scales here is the idempotency key: a client-generated identifier, sent as a header, that the server uses to deduplicate requests. Stripe popularized this approach in their payments API, and it's worth studying not because Stripe is infallible but because they built an API that handles money — where duplicate operations have immediate, concrete consequences — and they chose to put the deduplication burden on the infrastructure rather than on the caller's hope that the network behaves.
This is a broader principle worth naming: the API design patterns that scale tend to be pessimistic about infrastructure and optimistic about consumers. They assume the network will misbehave. They assume traffic will spike. They assume the data will grow. They don't assume bad faith from the developers calling the API; they try to make the right behavior the easy behavior.
Rate Limiting as Communication, Not Punishment
Rate limiting is usually implemented as a defensive measure — a wall to keep bad actors out or to prevent a single client from degrading the service for everyone else. That framing is incomplete, and it leads to rate limiting that's technically functional but practically hostile.
A rate limit without clear communication is just a mystery error. The API design patterns that scale treat rate limiting as a conversation: Retry-After headers that tell the client exactly when to try again, X-RateLimit-Remaining headers that let clients self-throttle before they hit the wall, response bodies that explain the limit in human-readable terms. The goal is a client that never has to guess why its request failed or how long to wait.
This extends to error design generally. An API with consistent, structured error responses — a stable shape, a machine-readable error code, a human-readable message — is an API that clients can handle gracefully. An API that returns a 500 with an HTML stack trace, or a 200 with an error message buried in the body, is an API that forces every consumer to write bespoke parsing logic for failure cases. That logic will be wrong in at least three ways, and you'll find out about all three at 2 a.m.
The Long Game
I think about that first endpoint sometimes, the one that returned everything. It was built by someone — me — who was optimizing for the next two weeks rather than the next two years. That's a human thing to do. Deadlines are immediate; scale is hypothetical until it isn't.
The API design patterns that scale don't require clairvoyance. They require a particular kind of discipline: the willingness to make the right decision slightly harder to implement now so that it becomes dramatically easier to live with later. Version your API before you need to. Paginate before the dataset grows. Build idempotency before the network fails you. Communicate your limits before a consumer builds a broken integration around them.
What I've come to believe, after years of building and inheriting and autopsying APIs, is that the question isn't really about patterns at all. It's about who you think your future self is. Is that person someone you're willing to leave a good foundation for? Or are you going to leave them a JSON blob the size of a small novel and a comment that says // remove after Q3 migration?
The answer, I suspect, says something about more than just API design.