API Design Principles
February 19, 2026
REST vs GraphQL vs tRPC, versioning strategy, rate limiting, and documentation: a practical API design guide for startup engineering teams.
REST vs GraphQL vs tRPC
REST is the default choice for public APIs and the majority of internal ones. Resource-based URLs with HTTP verbs — GET /users/:id, POST /orders, DELETE /sessions — follow a convention that every developer understands, every HTTP client supports, and every API gateway can monitor, cache, and rate-limit without additional configuration. The familiarity of REST reduces onboarding time for new engineers and simplifies debugging because the request and response structure is predictable. If you're building an API that external developers will consume, REST is the only reasonable choice.
GraphQL solves a specific problem: clients that need to query for exactly the data they require, with no over-fetching or under-fetching. A mobile client that only needs a user's name and avatar won't receive a 50-field user object if the query specifies those two fields. The tradeoff is substantial complexity — the N+1 query problem (where a list query triggers one database call per item without a DataLoader), resolver depth limits that must be configured to prevent abuse, and a schema definition language that needs to be maintained separately from your database schema. GraphQL earns its complexity when you have multiple clients with different data requirements; for a single web application, it's usually unnecessary overhead. tRPC is the third option: a TypeScript-native RPC framework where the server's function signatures are the API contract. No schema maintenance, no code generation, end-to-end type safety between client and server. It works only when both sides of the API are TypeScript, which makes it an excellent choice for full-stack TypeScript applications using Next.js and a TypeScript backend, but not for public APIs or non-TypeScript consumers.
Versioning Strategy
API versioning is a commitment to stability: once you version an endpoint, you're promising that existing clients won't break when you evolve the API. The three common strategies each have different tradeoffs. URL path versioning (/v1/users, /v2/users) is the most explicit and easiest to test — you can open both versions in a browser, route them independently in your reverse proxy, and deprecate v1 by simply removing the route. It creates URL proliferation but remains the simplest approach for most startups.
Header-based versioning (Accept: application/vnd.myapi+json;version=2) keeps URLs clean but requires clients to set custom headers, which many HTTP clients and proxies don't handle by default. Query parameter versioning (?version=2) is pragmatic for internal APIs where you control the clients, but awkward for public APIs because query parameters are typically reserved for filtering and sorting. Regardless of which strategy you choose, follow two rules: never make a breaking change without incrementing the version, and maintain the previous version for at least six months after releasing the new one. A deprecation notice in the response headers — Deprecation: true and Sunset: Sat, 01 Jan 2027 00:00:00 GMT — gives API consumers advance warning without requiring out-of-band communication.
Rate Limiting
Rate limiting protects your API from accidental or intentional abuse. Return HTTP 429 Too Many Requests when a client exceeds their limit, with a Retry-After header specifying how many seconds to wait before the next request. The Retry-After header converts a confusing error into an actionable instruction and prevents clients from immediately retrying, which would amplify the load rather than reducing it.
Implement rate limits at three independent levels: per user (prevents a single account from overloading the API), per IP address (prevents a single server from overloading the API even when unauthenticated), and per endpoint (rate limiting a password-reset endpoint at 5 requests per hour regardless of who is making the request). These three levels have different limits — a user might have 1,000 requests per minute globally but only 10 requests per minute on the payment endpoint. Redis with a sliding window algorithm is the standard implementation: the token bucket or leaky bucket algorithms are both well-documented and available as libraries for every major language. Upstash provides managed Redis with a rate-limiting SDK that takes 15 lines of code to implement in a Next.js API route.
Documentation
An API without documentation is not a product; it's a riddle. OpenAPI (formerly Swagger) is the standard machine-readable format for describing REST APIs: it defines endpoints, parameters, request bodies, response schemas, and authentication methods in a YAML or JSON file. Generate the OpenAPI spec from your code using decorators (NestJS, FastAPI) or middleware rather than writing it by hand — hand-written specs drift from the implementation within weeks. Host the spec with Redoc or Swagger UI, both of which generate interactive documentation that developers can use to explore and test the API without writing code.
A Postman collection is a useful supplement for developer experience: it provides ready-to-run examples with real credentials and test data that developers can import and execute immediately. For internal APIs used only by your own team, an OpenAPI spec in the repository with a local Swagger UI instance is sufficient. For public APIs, add three additional documentation components: a getting-started guide that walks through authentication and a first API call in five minutes, an API changelog that records breaking changes by version, and code examples in the two or three most popular languages for your audience (JavaScript, Python, and cURL cover 80% of developer use cases).
Frequently Asked Questions
When should I version my API from the start vs adding versioning later? Add versioning from the first public release. Retrofitting versioning onto an existing API that clients depend on requires coordinating the migration with every API consumer simultaneously, which is painful. The cost of starting with /v1/ paths is zero; the cost of adding versioning to an unversioned API with live customers is days of migration work and significant customer support load.
How do I authenticate REST API requests? Bearer token authentication using JSON Web Tokens (JWTs) is the standard for most modern APIs. The client includes an Authorization: Bearer header on every request. JWTs are stateless — the server validates the signature without a database lookup — making them fast and horizontally scalable. For machine-to-machine authentication (webhooks, server-to-server integrations), use API keys with a hash stored in the database rather than storing the raw key. Never store raw API keys in your database; store only the SHA-256 hash and compare against the hash on each request.
What HTTP status codes should I use for error responses? Use 400 Bad Request for validation errors (the client sent invalid data), 401 Unauthorized when authentication is missing or invalid, 403 Forbidden when the user is authenticated but doesn't have permission, 404 Not Found when the resource doesn't exist, 409 Conflict when the request conflicts with current state (duplicate email on registration), 422 Unprocessable Entity for semantic validation failures, and 500 Internal Server Error for unexpected server-side failures. Include a consistent error response body with a machine-readable error code and a human-readable message.
Should I use HATEOAS in my REST API? HATEOAS (Hypermedia as the Engine of Application State) — including links to related actions in API responses — is theoretically elegant but practically unused. Almost no real-world REST APIs implement it, and no client library relies on it. Build a clean, consistent REST API with good documentation instead; the documentation provides the discoverability benefits that HATEOAS promises without the implementation complexity.
How do I handle large API payloads efficiently? Pagination is the first tool: never return unbounded lists. Use cursor-based pagination (next_cursor and previous_cursor in the response) rather than offset-based pagination for large datasets — offset pagination degrades as the offset grows because the database must skip an increasing number of rows. For large file uploads or downloads, use presigned S3 URLs rather than streaming through your API server; the client uploads directly to S3, and your server only handles the metadata. Compress responses with gzip; the Accept-Encoding: gzip header requests compression and most HTTP clients handle decompression automatically.