‹ Back to Blog Engineering

API Design Best Practices for Modern Applications

April 1, 2026 · 10 min read
API analytics dashboard

An API is a contract. It is a promise you make to every developer who integrates with your system about how your service will behave. Breaking that contract has consequences: broken integrations, angry partners, costly migrations, and lost trust. Designing the contract well from the start is one of the highest-leverage activities in software engineering.

At Pepla, APIs are central to everything we build. Our services communicate through APIs, our clients integrate through APIs, and Pepla Voice exposes APIs for configuration and analytics. This article distils what we have learned about designing APIs that developers enjoy using and that stand the test of time.

REST Conventions: The Foundation

REST is not a standard; it is an architectural style. But the industry has converged on a set of conventions that, when followed consistently, produce APIs that are intuitive and predictable.

Resources are nouns, HTTP methods are verbs -- this distinction is fundamental but frequently violated.

API code

Resource Naming

Resources should be nouns, not verbs. The HTTP method provides the verb. This distinction is fundamental but frequently violated:

Use plural nouns for collections (/orders) and identifiers for individual resources (/orders/123). Nest sub-resources where there is a clear parent-child relationship: /orders/123/line-items. But avoid nesting more than two levels deep. If you find yourself writing /customers/456/orders/123/line-items/7/notes, the resource model needs rethinking.

HTTP Methods and Status Codes

Use HTTP methods for their intended semantics:

Status codes communicate the result unambiguously. Do not return 200 for everything with an error flag in the body. Use the codes as intended:

Pagination: Handle Large Collections

Any endpoint that returns a collection must support pagination. Returning 50,000 records in a single response is a denial-of-service attack on your own API. There are three common approaches:

Offset-Based Pagination

GET /orders?offset=20&limit=10 returns items 21-30. Simple and intuitive, but problematic for large datasets: the database still has to skip the first N rows, and concurrent inserts or deletes can cause items to be skipped or duplicated between pages.

Cursor-Based Pagination

GET /orders?after=abc123&limit=10 uses an opaque cursor (typically an encoded primary key or timestamp) to mark the position. This is performant at any depth and handles concurrent modifications gracefully. It is the approach we recommend for most APIs.

Page-Based Pagination

GET /orders?page=3&pageSize=10 is the most user-friendly but has the same underlying issues as offset-based. It works well for human-facing UIs where the total page count is displayed.

Always include pagination metadata in the response: total count (when feasible), current page or cursor position, and links to the next and previous pages. The response should contain everything the client needs to navigate the collection without constructing URLs manually.

Use nouns for resources, HTTP verbs for actions, and consistent naming across every endpoint.

Filtering and Sorting

Use query parameters for filtering: GET /orders?status=pending&createdAfter=2026-01-01. For complex filters, consider a structured query parameter: GET /orders?filter[status]=pending&filter[total][gte]=1000.

Testing APIs

Sorting follows a similar pattern: GET /orders?sort=-createdAt,+total where the prefix indicates direction. Document your sorting and filtering capabilities explicitly; do not make consumers guess which fields are filterable.

Versioning: URL vs Header

APIs evolve. Breaking changes are inevitable. The question is how to manage them.

URL Versioning

GET /v2/orders/123 is the most common approach. It is explicit, easy to route, and immediately visible in logs and documentation. The downside is that it pollutes URLs and encourages large, infrequent version bumps rather than incremental evolution.

Header Versioning

Accept: application/vnd.pepla.v2+json keeps URLs clean and supports content negotiation. It is harder to test with a browser and less visible in access logs. It works well for internal APIs where consumers are sophisticated.

Our Recommendation

For public APIs: URL versioning. The clarity and simplicity outweigh the aesthetic cost. For internal APIs: consider header versioning or no explicit versioning at all, using additive changes and feature flags instead. At Pepla, our public APIs use URL versioning with a commitment to supporting each major version for at least 18 months after the next version launches.

Error Responses: Be Helpful

Error responses are where most APIs fail their consumers. A bare 400 Bad Request with no body is useless. A well-structured error response should include:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request body contains invalid fields.",
    "details": [
      {
        "field": "email",
        "issue": "Must be a valid email address.",
        "value": "not-an-email"
      },
      {
        "field": "quantity",
        "issue": "Must be a positive integer.",
        "value": -5
      }
    ],
    "traceId": "abc-123-def-456",
    "documentation": "https://docs.pepla.co.za/errors/VALIDATION_FAILED"
  }
}

Every error should have a machine-readable code (for programmatic handling), a human-readable message (for developers debugging), field-level details (for form validation), a trace ID (for support escalation), and a documentation link (for self-service resolution).

Error responses should help developers fix problems -- include codes, messages, and guidance.

Rate Limiting

Every public API needs rate limiting. Without it, a single misbehaving client can degrade the service for everyone. Implement rate limiting using the standard headers:

Rate limits should be per API key or per authenticated user, not per IP address. IP-based limiting breaks for clients behind NATs and corporate proxies. Use a sliding window algorithm (like token bucket) rather than fixed windows to prevent burst issues at window boundaries.

Documentation: OpenAPI and Beyond

An undocumented API is an unusable API. OpenAPI (formerly Swagger) has become the standard for API documentation, and for good reason: it is machine-readable, supports code generation, and integrates with testing tools.

Good API documentation includes:

The best API documentation is generated from the code itself. Use annotations or decorators to keep your OpenAPI spec in sync with your implementation. Documentation that drifts from reality is worse than no documentation at all.

The best API documentation is generated from code itself -- specs that drift from reality are worse than none.

GraphQL: When and Why

GraphQL is not a replacement for REST. It solves a specific problem: when the consumer needs to fetch data from multiple related resources in a single request, and the shape of that data varies significantly between consumers.

GraphQL excels when:

REST is typically better when:

For most applications we build at Pepla, REST with well-designed resource endpoints, field selection (?fields=id,name,status), and compound documents (embedding related resources) provides 90% of GraphQL's benefits with significantly less operational complexity.

At Pepla, every API we build follows these conventions from day one. Our .NET and Spring Boot teams maintain shared API design guidelines that ensure consistency across all client projects.

APIs are products. They have users (developers), user experience (documentation and consistency), and lifecycles (versioning and deprecation). Treat them with the same design rigour you would apply to any user-facing product, and your integrations will be smoother, your partners happier, and your maintenance burden lighter.

Need help with this?

Pepla designs and builds APIs that developers love to integrate with. Let us design yours.

Get in Touch

Contact Us

Schedule a Meeting

Book a free consultation to discuss your project requirements.

Book a Meeting ›

Let's Connect