DEV Community

Preecha
Preecha

Posted on

API Design Principles: Building APIs Developers Actually Want to Use

APIs serve as the connective tissue of modern software, enabling disparate systems to communicate seamlessly. The difference between an API that developers embrace and one they tolerate comes down to design: thoughtful APIs accelerate development, reduce integration friction, and scale gracefully, while poorly designed APIs create bugs, confusion, and technical debt.

Try Apidog today

đź’ˇ Pro Tip: Transitioning to an API-design-first approach? Apidog provides an intuitive visual editor for designing endpoints, defining reusable components, and standardizing schemas. With built-in API design guidelines based on OpenAPI best practices and AI-powered compliance checks, Apidog helps you validate API quality before writing implementation code.

Understand API Design Fundamentals

API design is the process of deciding how software components communicate. It covers:

  • Endpoint and URL structure
  • HTTP methods and status codes
  • Request and response formats
  • Authentication and authorization
  • Validation and error handling
  • Versioning and backward compatibility
  • Documentation and testing workflows

These decisions should happen before implementation. Treating an API as a product—not an implementation detail—helps teams build interfaces around consumer use cases instead of exposing internal database structures.

A well-designed API should be:

  • Consistent: Similar operations follow similar patterns.
  • Simple: Each endpoint has a clear responsibility.
  • Predictable: Developers can infer behavior from what they already know.
  • Secure: Authentication, authorization, and validation are part of the initial design.
  • Documented: Consumers can understand and test the API without reading server code.

Apply Core API Design Principles

Keep naming and responses consistent

Use uniform naming conventions, predictable URL structures, and standardized response formats.

For example, if GET /users returns a collection, developers will expect GET /orders to follow a similar pattern. Avoid mixing arrays, objects, and unrelated wrapper formats without a clear reason.

Keep endpoints focused

Each endpoint should have a clear purpose. Endpoints that combine unrelated operations are harder to document, test, and maintain.

Separating responsibilities makes the API easier to understand and allows clients to use only the functionality they need.

Design security from the beginning

Authentication, authorization, and input validation should be part of the API contract. Retrofitting security later often results in inconsistent protection and vulnerabilities.

Use Resource-Oriented URLs

RESTful APIs organize around resources—conceptual entities that represent business objects. Resources are identified by URLs and manipulated with standard HTTP methods.

For an e-commerce API, common resources might include:

  • Products
  • Orders
  • Customers
  • Reviews

A resource-oriented endpoint structure could look like this:

GET    /products
GET    /products/{id}
POST   /products
PUT    /products/{id}
PATCH  /products/{id}
DELETE /products/{id}
Enter fullscreen mode Exit fullscreen mode

Use nouns for resource URLs. Let HTTP methods express the operation instead of embedding actions in the path.

Prefer:

GET  /users
POST /orders
Enter fullscreen mode Exit fullscreen mode

Instead of:

GET  /getUsers
POST /createOrder
Enter fullscreen mode Exit fullscreen mode

Separating resources from actions produces URLs that are easier to predict and document.

Model relationships carefully

Nested URLs can communicate resource relationships:

GET  /customers/{customer_id}/orders
POST /customers/{customer_id}/orders
Enter fullscreen mode Exit fullscreen mode

Keep nesting shallow. One or two levels usually provide useful context without producing unwieldy URLs:

/customers/{customer_id}/orders/{order_id}/items
Enter fullscreen mode Exit fullscreen mode

Deep nesting can indicate that the resource relationships need to be modeled differently.

Use HTTP Methods According to Their Semantics

HTTP methods have established meanings. Following those semantics improves predictability and enables clients, browsers, and proxies to behave correctly.

Method Purpose Idempotent Safe
GET Retrieve a resource representation Yes Yes
POST Create a resource or trigger a non-idempotent operation No No
PUT Replace an entire resource Yes No
PATCH Partially update a resource May vary No
DELETE Remove a resource Yes No

GET: retrieve data

GET requests should not modify server state.

GET /users/123
Enter fullscreen mode Exit fullscreen mode

Calling the same GET endpoint repeatedly should not create side effects. This allows responses to be cached, bookmarked, prefetched, or retried safely.

Avoid using GET to increment counters, send notifications, or update records.

POST: create resources

POST commonly creates a new resource:

POST /users
Content-Type: application/json

{
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Identical POST requests can create multiple resources. Clients should not automatically retry them after a network failure unless the API provides an additional mechanism, such as an idempotency key.

PUT: replace resources

PUT replaces the complete resource representation:

PUT /users/123
Content-Type: application/json

{
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "role": "admin"
}
Enter fullscreen mode Exit fullscreen mode

Because PUT is idempotent, repeating the request should produce the same final state. Make it clear whether omitted fields are reset, rejected, or assigned defaults.

PATCH: partially update resources

PATCH changes only the fields included in the request:

PATCH /users/123
Content-Type: application/json

{
  "role": "editor"
}
Enter fullscreen mode Exit fullscreen mode

PATCH behavior can vary. Replacing selected fields is typically idempotent, while operations such as incrementing a counter may not be. Document retry behavior for each PATCH operation.

DELETE: remove resources

DELETE /users/123
Enter fullscreen mode Exit fullscreen mode

DELETE is idempotent when repeated requests produce the same final state: the resource no longer exists. The first request may remove the resource; subsequent requests may return 404 or another documented response while leaving the final state unchanged.

Return Useful Status Codes

Status codes provide immediate information about the result of a request.

Category Range Meaning
2xx 200–299 Success
4xx 400–499 Client error
5xx 500–599 Server error

Common responses include:

  • 200 OK: A successful request that returns a response body.
  • 201 Created: A resource was created, often with a Location header.
  • 204 No Content: The operation succeeded without a response body.
  • 400 Bad Request: The request is malformed or contains invalid input.
  • 401 Unauthorized: Authentication is missing or failed.
  • 403 Forbidden: The authenticated caller lacks permission.
  • 404 Not Found: The resource does not exist or is intentionally hidden.
  • 429 Too Many Requests: The client exceeded a rate limit.
  • 500 Internal Server Error: An unexpected server-side failure occurred.

Use 4xx responses for issues the client can fix and 5xx responses for failures that require server-side investigation. Avoid returning 200 OK with an error object in the response body; that forces every client to inspect both the status code and payload.

Standardize error responses

Use one error structure throughout the API:

{
  "error": "VALIDATION_FAILED",
  "message": "The request body contains invalid data",
  "details": [
    {
      "field": "email",
      "issue": "Invalid email format"
    },
    {
      "field": "password",
      "issue": "Must be at least 8 characters"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

A useful error response includes:

  1. A machine-readable error code.
  2. A human-readable message.
  3. Field-level details when validation fails.

Clients can use the error code for programmatic handling and the details to display actionable messages to users.

Choose a Versioning Strategy

APIs evolve as new features are added and breaking changes become necessary. Versioning lets you improve an API without unexpectedly disrupting existing consumers.

URI versioning

Place the version in the URL path:

GET /v1/users
GET /v2/users
Enter fullscreen mode Exit fullscreen mode

This approach is easy to discover, test, and debug. Developers can see the version directly in the URL.

Header-based versioning

Place the version in an HTTP header:

GET /users
Accept: application/vnd.myapi.v2+json
Enter fullscreen mode Exit fullscreen mode

URLs remain stable, but testing requires a client that supports custom headers.

Query parameter versioning

Place the version in the query string:

GET /users?version=2
Enter fullscreen mode Exit fullscreen mode

This is simple to implement and test, but it mixes versioning with other query parameters such as filtering and pagination.

The strategy matters less than applying it consistently. Document:

  • Which versions are available
  • Which version is the default
  • How long older versions are supported
  • What changed between versions
  • Which changes are breaking

Build Security into the API Contract

API security protects sensitive data and prevents unauthorized actions.

Authentication

Authentication verifies who is making a request. Common approaches include:

  • API keys: Often used for server-to-server communication.
  • OAuth 2.0: Used for delegated access.
  • JSON Web Tokens (JWT): Signed tokens that can carry identity and permission information.

Authorization

Authorization determines what an authenticated identity can do. Role-Based Access Control (RBAC) assigns permissions to roles and roles to users.

For example:

  • A customer can access only their own orders.
  • Support staff can view orders across customers.
  • Administrators can manage users and permissions.

Always enforce authorization at the resource level. Checking only whether a user is authenticated is not enough.

Enforce HTTPS

All API traffic should use HTTPS. Unencrypted HTTP can expose credentials, tokens, and sensitive data to anyone monitoring the network.

Enforce HTTPS at the infrastructure level and redirect or reject plain HTTP requests.

Add rate limiting

Rate limiting protects against abuse and accidental overload. Limits can apply per user, IP address, or API key.

When a client exceeds its limit, return 429 Too Many Requests and include retry information:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699887600
Enter fullscreen mode Exit fullscreen mode

Validate all input

Validate every input field against expected:

  • Data type
  • Format
  • Length
  • Range
  • Allowed values

Reject malicious or invalid payloads with clear errors, but do not expose stack traces, SQL statements, internal paths, or other implementation details.

Handle Large Datasets with Pagination

Returning thousands of records in a single response increases server load, response time, and client memory usage. Pagination breaks large collections into manageable responses.

Offset-based pagination

Offset pagination uses a starting position and page size:

GET /products?offset=20&limit=20
Enter fullscreen mode Exit fullscreen mode

It is intuitive and supports jumping to arbitrary pages. However, large offsets can perform poorly, and records may be duplicated or skipped if the dataset changes between requests.

The original request contained a duplicated GET /products? prefix; the intended request is:

GET /products?offset=20&limit=20
Enter fullscreen mode Exit fullscreen mode

Cursor-based pagination

Cursor pagination uses an opaque token representing the current position:

GET /products?limit=20
Enter fullscreen mode Exit fullscreen mode
{
  "data": [
    {
      "id": 1,
      "name": "Example product"
    }
  ],
  "next_cursor": "eyJpZCI6MjB9"
}
Enter fullscreen mode Exit fullscreen mode

The client uses the cursor to fetch the next page:

GET /products?cursor=eyJpZCI6MjB9&limit=20
Enter fullscreen mode Exit fullscreen mode

Cursor pagination works well for real-time or frequently changing datasets. It is less suitable when clients need to jump directly to an arbitrary page.

Use offset pagination for static datasets and occasional browsing. Use cursor pagination for feeds and sequential consumption.

Treat Documentation as a Design Artifact

Documentation is the primary interface between an API and its consumers. Even a well-designed API becomes difficult to use when its behavior is undocumented.

OpenAPI Specification, formerly known as Swagger, provides a machine-readable description of an API. It can describe:

  • Endpoints and HTTP methods
  • Parameters
  • Request bodies
  • Authentication requirements
  • Response schemas
  • Error responses

Tooling can use an OpenAPI definition to generate interactive documentation, client libraries, and server stubs.

At minimum, document:

  1. What the API does and who should use it.
  2. How to obtain and use credentials.
  3. Every endpoint, method, parameter, and request body.
  4. Success and error response formats.
  5. Common workflows and use cases.
  6. Code examples in languages your consumers use.
  7. Pagination, filtering, sorting, and versioning behavior.

Interactive documentation that lets developers make live requests can reduce setup time and make the API easier to evaluate.

Avoid Common API Design Pitfalls

Mixing actions into URLs

Avoid endpoints such as:

GET  /getUsers
POST /createOrder
Enter fullscreen mode Exit fullscreen mode

Use resource-oriented URLs instead:

GET  /users
POST /orders
Enter fullscreen mode Exit fullscreen mode

Ignoring HTTP semantics

A GET endpoint that modifies data can trigger unintended changes when browsers, crawlers, or proxies prefetch or cache requests.

Returning inconsistent errors

Different error formats across endpoints force clients to implement multiple parsing paths. Define one error schema and use it consistently.

Creating chatty APIs

Requiring separate requests for a user, profile, preferences, and settings can add unnecessary latency.

Where appropriate, design responses that return related data together:

GET /users/123?include=profile,preferences
Enter fullscreen mode Exit fullscreen mode

The exact inclusion mechanism should be documented and used consistently.

Over-fetching data

Returning a complete user object when the client needs only a name and ID wastes bandwidth and processing time. Field selection can help:

GET /users?fields=id,name,email
Enter fullscreen mode Exit fullscreen mode

Only add field selection when its behavior can be specified clearly, including how invalid or unavailable fields are handled.

Choose Between Design-First and Code-First

Design-first

A design-first workflow creates the API specification before implementation.

Typical steps are:

  1. Define resources and use cases.
  2. Write the OpenAPI contract.
  3. Review the contract with frontend, backend, and other stakeholders.
  4. Generate or configure mock responses.
  5. Implement against the approved contract.
  6. Test the implementation for contract compatibility.

This approach creates a shared contract and allows frontend and backend teams to work in parallel.

Code-first

A code-first workflow generates the API specification from implementation code.

The main advantage is that documentation can stay closely aligned with the code that produces it. The risk is that the resulting API may expose implementation details instead of being designed around consumer needs.

Use a hybrid workflow when appropriate

A practical approach is:

  • Use design-first for new APIs and major changes.
  • Generate specifications from existing APIs when documenting legacy systems.
  • Review generated specifications and adjust them to improve consumer usability.
  • Use contract testing to detect drift between the specification and implementation.

Use This API Design Checklist

Before implementation, verify that the API:

  • Uses consistent resource names and URL patterns.
  • Uses HTTP methods according to their semantics.
  • Returns appropriate status codes.
  • Defines a consistent error response.
  • Documents authentication and authorization behavior.
  • Requires HTTPS.
  • Validates all input.
  • Defines rate limits and retry behavior.
  • Uses pagination for large collections.
  • Defines a versioning and deprecation strategy.
  • Documents request and response examples.
  • Has an OpenAPI definition or equivalent contract.
  • Can be tested independently by API consumers.

The Path Forward

API design shapes how systems interact for years. Decisions made during design affect maintenance, integrations, reliability, and developer productivity.

Consistency, simplicity, security, clear errors, pagination, versioning, and documentation provide a strong foundation. The implementation details will vary by team and use case, but the goal remains the same: make the API clear, predictable, and easy to use.

APIs exist to be consumed. Prioritize the developer experience, validate the contract early, and design interfaces that developers can understand without reverse-engineering the server.

Top comments (0)