PRODUCT BUILDERJakub KulhanJK
← Skills library

API design

Designing how systems communicate – both internal service APIs and external-facing APIs that others build on.

Solution DiscoveryEngineerProduct Builder
API design
ACTIVITIES

What you need to do

The practical activities and techniques you need to master – what to practice, what to get good at, and what to keep doing.

Choosing API type

The first design decision isn't endpoint names – it's which API paradigm fits your use case. REST for public APIs and simple CRUD – universally understood, excellent tooling, cacheable by default. RPC (gRPC, tRPC) for internal service-to-service communication or when your frontend and backend share a TypeScript codebase. GraphQL when clients need flexible queries across complex, nested data. Webhooks for event-driven integrations where your system pushes data to external consumers. Most products use REST for 90% of their needs.

Authentication and authorization

API keys for server-to-server communication – simple, no user context needed. Session cookies for web applications where the browser handles authentication automatically. JWTs for stateless authentication in distributed systems. OAuth 2.0 when third parties need delegated access. Don't invent your own auth scheme. Pick the pattern that matches your use case and use a well-tested library. The most common mistake is starting with JWTs when session cookies would be simpler and more secure.

Pagination

Every list endpoint needs pagination – unbounded result sets kill performance and break clients. Cursor-based pagination (pass the last item's ID as a cursor) is the most robust approach: stable results when data changes, no performance degradation on deep pages, and works with real-time data. Offset-based pagination (page=3&limit=20) is simpler but breaks when items are inserted or deleted between requests. Use consistent field names across every endpoint: cursor, limit, hasMore. Clients should write the pagination logic once and it works for every resource.

Errors

Every API error should return a consistent shape – a status code, an error code (machine-readable), a message (human-readable), and optionally a details array for field-level validation errors. Never return 200 with an error message in the body. Never expose stack traces or internal implementation details. Use problem+json (RFC 9457) as a standard error format. Document every error code your API can return. Consistent errors are what separate an API that developers love from one that makes them write workaround after workaround.

Rate limiting

Rate limiting protects your API from abuse and ensures fair access. Use token bucket or sliding window algorithms – most frameworks have middleware for this. Return 429 (Too Many Requests) with a Retry-After header when limits are exceeded. Include rate limit headers in every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Differentiate limits by tier – free users get 100 requests/minute, paid users get 1,000.

API evolution

Never make breaking changes to a published API. Add new fields, don't rename or remove existing ones. Add new endpoints, don't change the behavior of existing ones. When you must make breaking changes, use API versioning – URL path versioning (/v1/users, /v2/users) is the simplest and most visible approach. Deprecate before removing: mark the old version as deprecated, give clients a migration timeline, monitor usage, and remove only when traffic drops to zero.

KEY FRAMEWORKS

Frameworks to know

The mental models and structured approaches that guide how you think about this skill – not rules to follow, but lenses to see through.

REST

Representational State Transfer – the dominant API paradigm for web applications and public APIs. Design around resources (nouns), not actions (verbs): /orders, /users, /products. Use HTTP methods as intended: GET reads, POST creates, PUT replaces, PATCH updates, DELETE removes. Return appropriate status codes: 200 for success, 201 for created, 400 for client errors, 404 for not found, 500 for server errors. Make operations idempotent where possible. The goal is an API that a developer can guess correctly before reading the docs. The Richardson Maturity Model defines four levels: Level 0 (one endpoint), Level 1 (resources), Level 2 (HTTP methods), Level 3 (hypermedia). Most production APIs sit at Level 2.

RPC

Remote Procedure Call – APIs designed as function calls rather than resource operations. gRPC uses Protocol Buffers for schema-enforced contracts with binary serialization – ideal for internal service-to-service communication where performance matters. tRPC provides full type safety when your frontend and backend share a TypeScript codebase – zero schema overhead, types inferred directly from server code. RPC shines when your API represents actions ('processPayment', 'sendNotification') rather than resources, or when you need streaming support.

GraphQL

A query language that lets clients request exactly the data they need in a single request. Clients define the shape of the response – no over-fetching (getting fields you don't need) and no under-fetching (making multiple requests for related data). GraphQL uses a strongly-typed schema that defines every type, query, and mutation. Best suited for complex UIs that aggregate data from multiple entities – mobile apps, dashboards, and client-heavy applications. Trade-offs: caching is harder than REST, N+1 query problems require DataLoader, and authorization per field adds complexity.

Webhooks

HTTP callbacks that push data to external systems when events occur – the foundation of event-driven integrations. When something happens in your system, you POST a payload to a URL the client configured. Design them like events, not commands: 'order.created', 'payment.failed', 'user.updated'. Include a timestamp, event type, and the full resource in the payload. Implement retry logic with exponential backoff for failed deliveries. Sign every payload with HMAC so clients can verify authenticity. Provide a webhook testing endpoint so developers can verify their integration without triggering real events.

THE PRODUCT BUILDER WAY

Good vs. bad

Common mistakes paired with what experienced practitioners do instead – and why the product builder approach changes the game.

Verbs in URLs
POST /api/createUser, GET /api/getUserById, POST /api/updateOrder. The API is a collection of RPC calls disguised as REST. Every new action needs a new endpoint name, and there's no consistent pattern to guess.
Resources with HTTP methods
POST /users, GET /users/:id, PATCH /orders/:id. The URL is the resource, the method is the action. Developers can predict endpoints before reading the docs.
200 OK with error in body
Every response returns 200 with { success: false, error: 'Not found' }. HTTP clients, caches, and monitoring tools all think every request succeeded. Error handling requires parsing the body of every response.
Proper status codes with consistent error shape
404 with { code: 'NOT_FOUND', message: 'Order not found', details: [] }. HTTP semantics work correctly – clients catch errors in the error path, monitoring tracks real failure rates.
Breaking changes without versioning
Renaming a field from 'userName' to 'username' in a Friday deploy. Every client that uses the old name breaks silently – no error, just missing data.
Additive changes with deprecation notices
Add 'username', keep 'userName', mark it deprecated in the docs. Give clients a migration timeline. Monitor usage of the old field. Remove it when traffic hits zero.
No API specification
The API documentation is a Notion page that's three months out of date. Frontend developers discover endpoints by reading backend source code. Every new integration starts with 'let me check what the API actually returns.'
OpenAPI spec as single source of truth
A YAML file in the repo that generates interactive docs, TypeScript types, and client SDKs. The spec is tested in CI – if the implementation doesn't match the contract, the build fails.
JWT for everything
Using JWTs for a server-rendered web app where session cookies would be simpler and more secure. Now you're handling token refresh, XSS-safe storage, and token revocation – problems that cookies solve for free.
Auth pattern matched to the use case
Session cookies for web apps (browser handles everything), JWTs for mobile/SPA with a token refresh flow, API keys for server-to-server, OAuth for third-party integrations. Each pattern solves different problems.
Inconsistent pagination
Some endpoints use offset/limit, some use page/per_page, one uses cursor-based pagination. Every client integration is a special case.
One pagination pattern everywhere
Cursor-based pagination with consistent field names (cursor, limit, hasMore) across every list endpoint. Clients write the pagination logic once and it works for every resource.

API design is where product builders make or break the developer experience of their product. A well-designed API is a force multiplier – it enables integrations, third-party apps, and automations that you never planned for. A poorly designed API becomes a support burden that consumes more engineering time than the features it exposes.

REST is the right default for almost everything. I've watched teams adopt GraphQL because it sounded modern, then spend months solving problems they wouldn't have had with REST – caching, N+1 queries, authorization per field, query cost analysis. GraphQL solves a real problem (over-fetching in complex UIs), but if your API is CRUD over a dozen resources, REST with well-designed endpoints is faster to build, easier to cache, and simpler to secure.

tRPC is the most underrated tool in the TypeScript product builder's toolkit. When your frontend and backend share a codebase, tRPC gives you full type safety with zero schema overhead – no OpenAPI spec, no code generation, no separate type definitions. You change a return type on the server and the client shows a type error immediately. For internal APIs in a monorepo, it's strictly better than REST.

The Stripe API is the best teacher of API design. Study their naming conventions (always plural resources, always snake_case), their error handling (every error tells you what went wrong AND how to fix it), their pagination (cursor-based, consistent everywhere), and their idempotency keys (safe retries built into the protocol). You don't need to read a book on API design – just study Stripe's API reference and apply the same principles.

I've seen more production incidents caused by API versioning mistakes than by any other API design decision. The rule is simple: never make breaking changes to a published endpoint. Add new fields, add new endpoints, deprecate old ones. If you must make a breaking change, version the URL (/v1/users → /v2/users) and support both until migration is complete. The expand-and-contract pattern from database migrations applies perfectly to API evolution.