Topic Overview

API Design

Design RESTful APIs that are intuitive, scalable, and maintainable. Learn versioning, error handling, and best practices.

25 min read

API Design

Why Engineers Care About This

APIs are contracts between services. When you design an API, you're making promises: "This endpoint will always return data in this format," "This parameter will always work this way," "This error code means this specific thing." When you break these promises, you break every service that depends on your API. Good API design is about making these contracts clear, stable, and easy to use.

When your API is confusing, or breaking changes force clients to rewrite code, or error messages don't help debug issues, you're hitting API design problems. These problems compound over time. A poorly designed API becomes a liability—every change risks breaking clients, every new feature requires workarounds, and every bug report requires explaining "that's just how the API works."

In interviews, when someone asks "How would you design an API for X?", they're really asking: "Do you understand that APIs are long-term commitments? Do you know how to version APIs, handle errors gracefully, and design for change?" Most engineers don't. They design APIs that work today but break tomorrow, or APIs that are so rigid that adding features requires breaking changes.

Core Intuitions You Must Build

  • APIs are forever, code is temporary. Once you publish an API, clients depend on it. You can't just change it. Versioning lets you evolve APIs without breaking existing clients, but versioning is expensive (you maintain multiple versions). Design APIs that can evolve without versioning when possible. Use extensible formats (JSON with optional fields), avoid tight coupling (don't require specific field orders), and plan for change.

  • Consistency is more important than perfection. A consistent API is easier to learn and use than a perfect API that's inconsistent. Use consistent naming (camelCase or snake_case, but not both), consistent error formats, consistent status codes. When developers learn one endpoint, they should be able to predict how other endpoints work. Consistency reduces cognitive load.

  • Error messages are user-facing, even for APIs. When an API returns an error, a developer reads that error message. Good error messages explain what went wrong, why it went wrong, and how to fix it. Bad error messages are cryptic ("Error 500") or misleading ("Invalid input" when the input is valid but the server is down). Design error responses that help developers debug issues.

  • REST is about resources, not actions. REST APIs model resources (users, orders, products) and use HTTP methods (GET, POST, PUT, DELETE) to operate on them. /users/123 is a resource. GET /users/123 retrieves it. DELETE /users/123 deletes it. Don't design /getUser or /deleteUser—those are RPC-style, not REST. REST's resource-based model scales better and is easier to cache.

  • Versioning is about compatibility, not features. API versioning isn't about adding new features—it's about maintaining compatibility. If you can add a feature without breaking existing clients, don't version. If you must break compatibility (remove a field, change behavior), then version. Use URL versioning (/v1/users) or header versioning (Accept: application/vnd.api+json;version=1), but be consistent.

  • Rate limiting protects your API, but it's user-facing. Rate limiting prevents abuse, but it also affects legitimate users. Design rate limits that are generous enough for normal use but strict enough to prevent abuse. Return clear headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) so clients can handle rate limits gracefully. Don't just return 429—tell clients when they can retry.

Subtopics (Taught Through Real Scenarios)

RESTful Design Principles

What people usually get wrong:

Most engineers think REST is "just using HTTP methods." But REST is about modeling your domain as resources and using HTTP's uniform interface (GET, POST, PUT, DELETE, PATCH) to operate on them. /users/123/orders is a resource (orders belonging to user 123). GET /users/123/orders retrieves them. POST /users/123/orders creates a new order. Don't design /getUserOrders or /createOrderForUser—those are RPC-style endpoints that don't leverage HTTP's semantics.

How this breaks systems in the real world:

A service had endpoints like /getUser, /createUser, /updateUser, /deleteUser. This worked fine initially. But as the service grew, they needed caching. HTTP caching works with GET requests, but /getUser wasn't a standard GET, so caches didn't work. They needed to add custom caching logic. Also, load balancers and proxies expect RESTful patterns—they can route /users/123 intelligently, but /getUser?id=123 requires custom routing. The fix? Refactor to RESTful endpoints (GET /users/123, POST /users, PUT /users/123, DELETE /users/123). But the real lesson is: REST isn't just style—it's about leveraging HTTP's built-in features (caching, idempotency, safety).

What interviewers are really listening for:

They want to hear you talk about resources, HTTP semantics, and leveraging HTTP features. Junior engineers say "REST is just using GET, POST, PUT, DELETE." Senior engineers say "REST models resources, uses HTTP's uniform interface, and leverages caching, idempotency, and safety guarantees." They're testing whether you understand that REST is about more than just HTTP methods—it's about designing APIs that work with the web's infrastructure.

API Versioning Strategies

What people usually get wrong:

Engineers often think "I'll just never change my API" or "I'll just break compatibility and force clients to update." But APIs evolve. Requirements change. You need to add fields, remove fields, change behavior. Versioning lets you evolve APIs without breaking clients, but versioning is expensive—you maintain multiple code paths, test multiple versions, and document multiple behaviors. The key is knowing when to version (breaking changes) vs when not to version (additive changes).

How this breaks systems in the real world:

An API added a new optional field to a response. This was an additive change—existing clients could ignore the new field. But the API team versioned anyway (/v2/users), thinking "new features need new versions." This created confusion: should clients use v1 or v2? What's the difference? The team maintained both versions for years, adding complexity. The fix? Only version for breaking changes. Additive changes (new optional fields, new endpoints) don't need versioning. But the real lesson is: versioning is expensive. Only version when necessary.

What interviewers are really listening for:

They want to hear you talk about when to version vs when not to version, and different versioning strategies. Junior engineers say "always version for new features." Senior engineers say "version only for breaking changes, use URL or header versioning consistently, and plan for version deprecation." They're testing whether you understand that versioning is a tool for compatibility, not a requirement for every change.

Error Handling and Status Codes

What people usually get wrong:

Most engineers return 200 OK for everything, or 500 for every error. But HTTP status codes communicate meaning. 200 means "success." 201 means "created." 404 means "not found." 400 means "bad request" (client error). 500 means "internal server error" (server error). Using the right status code helps clients handle errors correctly. Also, error responses should include details: what went wrong, why it went wrong, how to fix it. Don't just return "Error"—return structured error information.

How this breaks systems in the real world:

An API returned 200 OK for all responses, including errors. The response body contained { "success": false, "error": "..." }. This worked, but it broke HTTP semantics. HTTP caches cache 200 responses, so error responses were cached. Load balancers couldn't distinguish errors from successes, so they couldn't route away from failing servers. Monitoring tools couldn't detect errors (they look for non-2xx status codes). The fix? Use proper status codes (400 for client errors, 500 for server errors, 404 for not found). But the real lesson is: HTTP status codes aren't just conventions—they're part of HTTP's contract, and tools depend on them.

What interviewers are really listening for:

They want to hear you talk about proper status code usage, structured error responses, and how status codes affect caching and monitoring. Junior engineers say "just return 200 with an error field." Senior engineers say "use proper HTTP status codes, return structured error responses with details, and understand how status codes affect caching and monitoring." They're testing whether you understand that HTTP status codes are part of the API contract, not just numbers.

Request/Response Design

What people usually get wrong:

Engineers often design request/response formats that are convenient for the server but confusing for clients. They use inconsistent naming (camelCase in requests, snake_case in responses), return too much data (fetching a user returns their entire order history), or too little data (requiring multiple requests to get related data). Good API design balances server convenience with client usability. Use consistent naming, return reasonable amounts of data (with pagination for lists), and provide ways to request related data (query parameters, nested resources, or GraphQL-style field selection).

How this breaks systems in the real world:

An API returned user objects with nested order objects. Fetching a user returned their entire order history (potentially thousands of orders). This worked for users with few orders, but users with many orders caused huge responses (MBs of data). Clients had to parse and filter this data client-side. The fix? Use pagination for lists, and provide query parameters to include/exclude related data (?include=orders&orders_limit=10). But the real lesson is: design responses for the common case, but provide flexibility for edge cases. Don't assume clients want all data all the time.

What interviewers are really listening for:

They want to hear you talk about response design, pagination, and flexibility. Junior engineers say "just return all the data." Senior engineers say "design responses for common cases, use pagination for lists, provide query parameters for flexibility, and balance server convenience with client usability." They're testing whether you understand that API design is about serving clients, not just making server code easy.

Authentication and Authorization

What people usually get wrong:

Engineers often confuse authentication (who you are) with authorization (what you can do). Authentication verifies identity (username/password, API keys, OAuth tokens). Authorization checks permissions (can this user access this resource?). APIs need both. Also, authentication should be stateless when possible (JWT tokens) so APIs can scale horizontally. Don't store session state on servers—store it in tokens that clients send with each request.

How this breaks systems in the real world:

An API used server-side sessions for authentication. Each request required a session lookup. Under normal load, this worked. But during traffic spikes, the session store became a bottleneck. Also, load balancers couldn't route requests to any server (sessions were server-specific), so they had to use sticky sessions, which reduced load balancing effectiveness. The fix? Use stateless authentication (JWT tokens). Tokens contain user identity and can be verified without server-side lookups. But the real lesson is: stateless authentication scales better than stateful sessions, especially in distributed systems.

What interviewers are really listening for:

They want to hear you talk about authentication vs authorization, stateless vs stateful authentication, and security considerations. Junior engineers say "just use sessions" or "just use API keys." Senior engineers say "use stateless authentication (JWT) for scalability, implement proper authorization checks, and understand security trade-offs (token expiration, refresh tokens, scope limitations)." They're testing whether you understand that authentication and authorization are separate concerns with different scalability implications.


  • APIs are long-term contracts—design for stability and evolution
  • REST models resources—use HTTP methods to operate on resources, not actions
  • Version only for breaking changes—additive changes don't need versioning
  • Use proper HTTP status codes—they're part of HTTP's contract and affect caching/monitoring
  • Design responses for clients—balance server convenience with client usability
  • Stateless authentication scales better—use JWT tokens instead of server-side sessions
  • Consistency matters more than perfection—consistent APIs are easier to learn and use
  • Error messages are user-facing—design error responses that help developers debug issues

Key Takeaways

APIs are long-term contracts—design for stability and evolution

REST models resources—use HTTP methods to operate on resources, not actions

Version only for breaking changes—additive changes don't need versioning

Use proper HTTP status codes—they're part of HTTP's contract and affect caching/monitoring

Design responses for clients—balance server convenience with client usability

Stateless authentication scales better—use JWT tokens instead of server-side sessions

Consistency matters more than perfection—consistent APIs are easier to learn and use

Error messages are user-facing—design error responses that help developers debug issues


About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.