Candidates

Companies

Candidates

Companies

API Design Best Practices for Building RESTful APIs That Last

By

Liz Fujiwara

Stylized image of gears above a hand, used to depict building reliable, long‑lasting APIs.

Well designed REST APIs can remain in production for 5 to 10 years, surviving framework rewrites and client churn because the contract between server and client is stable and predictable. This durability comes from consistent resource modeling, predictable HTTP semantics, and clear versioning decisions, not from any single framework or technology choice. This article focuses on practical design practices for HTTP-based REST APIs aimed at engineers building long-lived services for web, mobile, and internal platforms.

Key Takeaways

  • Durable APIs come from consistent resource modeling, predictable HTTP semantics, strong error handling, and clear versioning decisions rather than any specific technology stack.

  • Good REST API design improves developer experience and system qualities like performance, observability, security, and long-term evolvability, while modern practices like OpenAPI-first design, structured errors, and observability headers are now standard.

  • REST is often used alongside GraphQL, webhooks, or event streaming, and designing effective APIs requires a product mindset that treats the API as a core business asset rather than just a technical interface.

Designing Clear Resource Models and RESTful URIs

A resource model is the set of domain objects exposed over HTTP, such as users, invoices, subscriptions, or projects, along with their relationships. This model should remain stable even if internal database schemas change or microservices are refactored. The external resource representation must deliberately diverge from internal table names and microservice boundaries to provide insulation against backend changes.

RESTful APIs should use nouns to represent resources in URIs, avoiding verbs, as HTTP methods already imply the action being performed. Use plural nouns for collections and stable identifiers for individual resources. For example, use /users for the collection and /users/12345 for a specific user. A well-designed URI should avoid verbs and instead focus on clear, descriptive nouns that represent resources, making the API intuitive and easier to use.

Resource URIs should reflect relationships using shallow nesting. A pattern like /projects/{projectId}/tasks communicates the relationship clearly. However, avoid deeply nested structures like /organizations/1/teams/7/projects/42/tasks/3 that become hard to evolve and force clients to know the full hierarchy.

Keep URIs lowercase with hyphen-separated words such as /billing-cycles or /access-tokens. Avoid special characters that complicate routing and documentation. Maintain consistent naming conventions such as camelCase or snake_case across endpoints and fields.

HATEOAS, or Hypertext as the Engine of Application State, allows clients to navigate resources and discover available operations through hyperlinks provided in responses, reducing the need for prior knowledge of URIs. The HATEOAS principle states that each HTTP GET request should return links to related resources, enabling dynamic API exploration. Implementing HATEOAS means the links returned can change depending on resource state, enabling a more flexible and adaptive API design.

Using HTTP Methods and Status Codes Consistently

Consistent method semantics make REST APIs predictable for client developers and easier to reason about during incident response. The HTTP protocol defines specific methods for specific operations, and adhering to established RESTful principles enhances the predictability of an API for developers.

The most common HTTP methods used in RESTful web APIs are GET, POST, PUT, PATCH, and DELETE, each corresponding to specific operations on resources:

  • GET: A GET request retrieves a representation of the requested resource at the specified URI. GET requests should be safe and idempotent, never modifying server state.

  • POST: A POST request is used to create resources or perform non-idempotent operations. A successful creation should return 201 with a Location header.

  • PUT: PUT requests are used to update an existing resource or create a new resource if it does not exist, and they must be idempotent, meaning multiple identical requests should have the same effect as a single request.

  • PATCH: A PATCH request performs partial updates to an existing resource, allowing clients to send only the changes rather than the entire resource representation.

  • DELETE: A DELETE request removes a resource at the specified URI and typically does not require a request body specified.

Standard HTTP status codes communicate outcomes clearly. Proper error handling in APIs involves returning consistent HTTP status codes along with informative messages to help clients understand and resolve issues. When an error occurs, the API should return appropriate HTTP status codes indicating whether it is a client error (4xx) or a server error (5xx).

Using standard headers like Content-Type, Accept, ETag, Cache-Control, and Authorization helps both browsers and non-browser clients behave correctly with minimal custom logic. The Content-Type header establishes the data format being exchanged, with most RESTful implementations defaulting to JSON.

Recommended HTTP Method and Status Code Combinations

The following table summarizes common operations, HTTP methods, expected success codes, and typical error codes for a RESTful API. Use this as a quick reference when designing your API correctly.

Operation

HTTP Method

Success Codes

Common Error Codes

Retrieve collection

GET

200

400, 401, 403, 500

Retrieve single resource

GET

200

401, 403, 404, 500

Create new resource

POST

201, 202

400, 401, 403, 409, 500

Full update (replace)

PUT

200, 204

400, 401, 403, 404, 409, 500

Partial update

PATCH

200, 204

400, 401, 403, 404, 409, 500

Remove resource

DELETE

200, 204

401, 403, 404, 500

Handling Requests, Responses, and Errors in a Predictable Way

Consistency in payload shapes, HTTP headers, and error formats reduces integration time and makes large systems easier to debug in cloud environments. A user-friendly API aims to reduce time-to-first-call for developers by making the contract predictable.

Use JSON as the default representation with application/json Content-Type. JSON data is language-neutral, easily parsed, and widely supported. When the API server supports other formats, use content negotiation via the Accept header. APIs should be stateless, containing all the necessary information for processing each client request without relying on stored session data.

Return only the data the consumer needs to reduce bandwidth in API responses. Clients can request sparse fieldsets using a fields parameter with comma-delimited values like fields=id,name,created_at. This approach reduces payload size significantly for web services handling large collection responses.

Designing a Consistent Error Response Schema

Error responses should not be improvised per endpoint. They should follow a standard contract agreed by backend and client teams. Good error handling in APIs involves returning clear, consistent error messages that help clients understand what went wrong and how to address the issue. Define consistent, structured JSON bodies for error messages that aid debugging.

Standardize error handling using formats like RFC 9457 for informative error responses. Common fields include:

  • type: A machine-readable error identifier or URI

  • message: A short, human-readable summary

  • status: The HTTP status code for reference

  • correlation_id: A unique identifier for tracing through distributed systems

  • field_errors: Optional validation error details

Here is a concrete JSON snippet showing a 400 validation error:

{

  "type": "VALIDATION_ERROR",

  "message": "Request validation failed",

  "status": 400,

  "correlation_id": "abc-123-xyz-789",

  "field_errors": {

    "email": "invalid format",

    "password": "must be at least 12 characters"

  }

}

Error messages should include enough information to help users correct their actions but must avoid exposing sensitive information that could be exploited by attackers. Stack traces, database names, and internal service names must never appear in production error responses, especially for 5xx standard error codes. Implementing consistent error handling practices in APIs enhances reliability and user experience, making it easier for clients to troubleshoot issues effectively.

Pagination, Filtering, and Partial Responses

Large collections must be paginated to avoid multi-megabyte responses and timeouts. When designing RESTful APIs, it is important to implement pagination and filtering to manage large datasets effectively, improving performance and reducing server load. Implementing pagination and query-based filtering allows clients to request only the subset of data they need.

Two common pagination styles exist:

  • Offset-based: Uses limit and offset query parameters. Pagination divides large datasets into smaller, manageable chunks, using query parameters like limit to specify the number of items to return and offset to specify the starting point. Simple but performs poorly on large tables.

  • Cursor-based: Uses limit and cursor or next_token parameters. More efficient for real-time feeds and large tables because the database can seek directly to the cursor position.

API responses should include metadata like total_count where feasible, and link-style fields such as next and previous URLs to simplify client implementations. Support HEAD requests on heavy endpoints so clients can fetch metadata without transferring full payloads.

Filter query parameters should be simple and predictable using patterns like status=active or created_before=2026-01-01. The query string handles filtering for existing data without requiring complex request bodies for read operations.

Versioning, Compatibility, and Long Term Evolution

API versioning is essential to ensure that changes do not break existing client applications, maintaining backward compatibility while introducing new features. Most production APIs over three years old have at least one breaking change, and a careful versioning strategy prevents disruptions for mobile apps, partners, and internal consumers.

Understanding different versions of changes matters:

  • Backward compatible: Adding optional fields, new endpoints, or new status codes

  • Breaking changes: Removing fields, changing data types, modifying semantics, removing endpoints

Breaking changes must be rare and deliberate. API versioning can increase complexity and maintenance efforts, so it should be implemented only for breaking changes to minimize disruption to existing clients.

Common REST API versioning strategies include:

  1. URI versioning: Include the version parameter in the path. One common method of API versioning is to include the version number in the URL path, such as /v1/resource or /v2/resource, which allows clients to specify which version they want to use.

  2. Header-based versioning: Another approach to API versioning is to use HTTP headers, where clients can specify the desired version in the Accept header, such as Accept: application/vnd.api.v1+json. You can also use a custom header like API-Version: 2.

  3. Query parameter versioning: Using query parameters for versioning, such as ?version=1, is generally discouraged as it is better suited for filtering or searching data rather than indicating API versions.

Pick one primary versioning style and document it clearly. Use semantic versioning at the contract level and maintain an API changelog so client teams know exactly when a field or endpoint changed behavior. Documentation should be maintained using tools like Swagger/OpenAPI to ensure up-to-date and accurate resources for developers.

Choosing a Versioning Strategy for Your Context

There is no single perfect versioning method for all teams. Choices depend on existing clients, caching layers, and documentation practices.

URI versioning is common for public APIs with third-party consumers that need explicit versioning and long-lived documentation, while header-based versioning is often used in internal microservices where stable URIs and flexible representation are preferred. Query parameter versioning is the least preferred for critical APIs due to caching and clarity concerns.

Header or media type versioning is attractive for internal microservice APIs or hypermedia-rich APIs that want stable URIs and more flexible evolution inside representations. This approach keeps URIs clean but requires clients to manage headers correctly.

Query parameter versioning interacts poorly with some caches and feels less explicit in api documentation, making it the least preferred option for critical APIs.

Contract-first workflows using OpenAPI help teams review and evolve APIs safely before rollout. Use OpenAPI/Swagger to provide interactive, machine-readable specifications that include working code examples and clear authentication guides. Many toolchains can generate client SDKs, tests, and documentation from the OpenAPI definition.

Security, Performance, and Observability in REST API Design

API design must consider security, performance, and observability alongside resource modeling. HTTPS with TLS is essential to protect data in transit. APIs should use OAuth 2.0 for authentication and enforce secure transport by default.

Role-based access control (RBAC) helps enforce least privilege access across users and services. Rate limiting protects infrastructure from abuse and ensures fairness across clients. When limits are exceeded, APIs should return 429 Too Many Requests along with headers like X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After. Rate limits should be tied to authenticated identities rather than only IP addresses.

Designing for Caching and Performance

Thoughtful use of caching reduces latency and infrastructure cost, particularly for high-read endpoints. Caching can significantly improve API performance by storing frequently accessed data in memory, reducing the need to query the database for every request. You can cache data at CDNs for seconds or minutes depending on freshness requirements.

Make GET responses cache-friendly by ensuring idempotence, stable URIs, and appropriate Cache-Control directives. Using HTTP headers like Cache-Control and ETag can help manage caching effectively, ensuring that clients receive the most up-to-date data while still benefiting from performance improvements. Efficient APIs minimize latency and reduce unnecessary resource consumption.

To optimize API performance, consider implementing asynchronous processing, which allows the API to handle multiple tasks simultaneously, improving responsiveness. This is especially useful for batch operations or heavy computations that might otherwise cause server overload.

Avoid cache-busting via ever-changing query strings unless necessary. Instead, prefer semantic identifiers like versioned images or assets. Monitor key performance indicators such as p95 and p99 latency, throughput, and error rates through dashboards and alerts.

Observability: Correlation IDs and Trace Context

In microservice architectures, a single HTTP request can trigger a chain of internal calls across multiple services. Consistent correlation or trace identifiers are critical for debugging and incident response.

Support standard or de facto headers like X-Request-ID, X-Correlation-ID, or W3C traceparent. Propagate them through downstream calls and include them in structured logs. Return the correlation ID back to clients in responses and error messages so support teams can use it when investigating issues.

Adopting distributed tracing tools that integrate with these headers helps identify hotspots, slow dependencies, and unexpected retries. This observability becomes essential when diagnosing failures across multiple related services.

REST vs GraphQL and Other Patterns in Modern API Design

Many teams use REST alongside GraphQL, webhooks, and asynchronous messaging. Successful API design means choosing the right tool for each use case rather than forcing one approach everywhere.

REST excels for:

  • Cacheable resource operations with clear HTTP semantics

  • Simple CRUD operations with straightforward process flows

  • Compatibility with HTTP infrastructure and CDNs

  • Easy onboarding for most developers building rest apis

  • Public APIs that must integrate easily with any programming language or client application

GraphQL shines for:

  • Client-driven querying with flexible data shapes

  • Reducing overfetching or underfetching in complex UIs

  • Aggregating multiple backends into a unified graph

  • Mobile applications with variable network conditions

Some organizations use REST for core transactional operations and public APIs, then layer GraphQL gateways on top for web and mobile applications that need to submit data requests across many related resources. Event-driven patterns like webhooks are often used alongside REST for real-time notifications, sending POST requests to client-provided callback URLs when a payment or subscription status changes.

Choosing REST for Durability and Simplicity

REST remains a strong default choice for external and internal APIs that need long-term stability, clear caching behavior, and compatibility with existing tooling. The REST architectural style provides a uniform interface that most developers understand.

Characteristics that point toward REST include:

  • Predictable resource lifecycles with well-defined business transactions

  • Heavy reliance on HTTP caching or CDN distribution

  • Need for clear API documentation that any developer can follow

  • Long-term support creation requirements for partners and integrations

Teams can start with REST and adopt GraphQL incrementally later. A good RESTful API can serve as the foundation for a GraphQL gateway that composes existing REST endpoints rather than replacing them. This approach preserves the simplicity of REST for building APIs while providing flexibility for frontend teams.

Conclusion

Long-lived REST APIs succeed because they are consistent, well documented, secure, observable, and versioned carefully, not because of any specific language or framework. Focusing on clear resource models, predictable HTTP behavior, structured error responses, and thoughtful versioning reduces breaking changes and production emergencies over time.

Apply these practices incrementally to existing APIs as well as new ones, using tools like OpenAPI, automated tests, and monitoring to validate improvements. Review one existing API in your organization this week, identify two or three design improvements from this article, and plan a small backlog of refactors to make the API more durable. Even if you can only fix one endpoint at a time, the cumulative effect builds APIs that last.

FAQ

What are the most important best practices for designing a RESTful API?

What common API design mistakes should engineers avoid?

How should I handle versioning, naming conventions, and error responses in API design?

What are the most useful RESTful API design patterns for production systems?

When should I choose REST vs GraphQL for my API design?