ApiaryActive
Try: pause · settings · learn · wipe
← Community / Reading Room
RA
craft · 12 min read

REST API Best Practices

Designing a RESTful HTTP service is a bit like tending a hive. A well‑structured API lets data flow smoothly, reduces friction for developers, and ultimately…

Designing a RESTful HTTP service is a bit like tending a hive. A well‑structured API lets data flow smoothly, reduces friction for developers, and ultimately supports the larger mission—whether that’s powering a bee‑conservation dashboard, coordinating autonomous AI agents, or delivering the next‑generation mobile app. Yet, as with any living system, small missteps can cascade into hard‑to‑debug bugs, broken clients, and costly rewrites.

In the past three years, a survey of 1,200 public APIs reported that 42 % of developers abandoned a product because of confusing endpoint layouts or opaque error messages. That churn is not just a statistic; it translates into lost time, wasted bandwidth, and, for platforms like Apiary, missed opportunities to amplify conservation data to the world. By grounding our API design in proven conventions—clear resource naming, disciplined versioning, thoughtful pagination, and precise error handling—we give both humans and machines a predictable, resilient interface.

The following guide distills the collective experience of API architects, the lessons learned from high‑traffic services (think GitHub’s 200 M requests / day, Stripe’s 1 B / month), and the unique considerations that arise when our endpoints serve ecological data or autonomous agents. It’s a practical handbook, not a manifesto: you’ll find concrete patterns, code snippets, and measurable trade‑offs you can apply to any HTTP service today.


1. Designing Resource URLs

1.1 Keep URLs as nouns, not verbs

REST treats the URL as a resource identifier—the location of a thing—while the HTTP method (GET, POST, PATCH, DELETE) describes the action. A classic anti‑pattern is /createUser or /deleteOrder. Instead, use:

GET    /users                # list users
POST   /users                # create a new user
GET    /users/12345          # retrieve user 12345
PATCH  /users/12345          # update user 12345
DELETE /users/12345          # delete user 12345

The rule of thumb: always start with a plural noun. This mirrors the way we talk about bee colonies (“the hive”, “the foragers”) and aligns with the expectations of HTTP clients and tools like curl or Postman.

1.2 Hierarchical nesting for relationships

When a resource is naturally scoped under another, reflect that hierarchy in the path. For an API that tracks pollination events per apiary:

GET /apiaries/42/pollinations          # all pollination records for apiary 42
POST /apiaries/42/pollinations          # add a new pollination event
GET /apiaries/42/pollinations/7         # specific event #7

Avoid deep nesting beyond two levels; each extra segment adds cognitive load and can break caching strategies. If you find yourself needing /countries/US/states/CA/cities/LA/parks, consider flattening with query parameters (/parks?city=LA) or a dedicated search endpoint.

1.3 Use hyphens, not underscores

Hyphens (-) are URL‑safe and improve readability. Browsers treat underscores (_) as word separators only in some contexts, which can lead to inconsistencies in analytics. A consistent style reduces the chance of a 404 caused by a typo.

Bad: /bee_population_data Good: /bee-population-data

1.4 Avoid trailing slashes

A trailing slash can be interpreted as a directory versus a file. Most modern frameworks treat them interchangeably, but the extra 301 redirects cost latency. Choose a convention (most APIs omit the trailing slash) and enforce it with middleware that returns a 301 Moved Permanently for mismatched requests.


2. Naming Conventions

2.1 Singular vs. plural

The consensus among large platforms (GitHub, Twitter, Shopify) is to use plural nouns for collections and singular nouns for individual items within those collections. This eliminates ambiguity when a client sees /bees (a list) versus /bees/123 (a single bee).

2.2 Consistent case

  • Lower‑case for everything. Upper‑case letters are technically allowed but can cause problems with case‑sensitive servers (e.g., Linux) and CDN edge caches.
  • Kebab‑case (kebab-case) for multi‑word segments (/apiary-locations).

2.3 Reserved words and future‑proofing

Avoid using words that may clash with future HTTP extensions (/cache, /auth). Instead, namespace them:

GET /v1/auth/tokens
POST /v1/cache/clear

If you anticipate a future feature, reserve a segment now (e.g., /v1/experimental/…) so you can roll it out without breaking existing clients.

2.4 Version as part of the path vs. header

Both approaches are valid, but putting the version in the URL (/v1/…) is the most discoverable for developers and tools. A header‑only versioning scheme (Accept: application/vnd.myapi.v2+json) can be more flexible but often hides the version from simple documentation generators.

Recommendation: Adopt path versioning for public APIs and reserve header versioning for internal micro‑service communication where strict contract enforcement is required.


3. Versioning Strategies

3.1 Semantic versioning in URLs

Use a major version in the path (/v1/…, /v2/…). Minor updates that remain backward compatible can be introduced without changing the URL, relying on feature flags or optional fields.

GET /v1/bees?include=last_seen   # v1 supports “last_seen” as optional
GET /v2/bees?include=last_seen   # v2 adds “last_seen” as default

3.2 Deprecation policy

A concrete deprecation timeline builds trust. For example, Stripe gives 90 days notice before retiring an endpoint, with a clear Deprecation header:

Deprecation: true
Deprecation-Effective-Date: 2027-01-01

Publish a deprecation notice in the API documentation and send webhook alerts to registered clients.

3.3 Migration helpers

Provide a sandbox environment (https://sandbox.apiary.org/v2/…) where developers can test against the new version. Offer a diff endpoint that returns schema changes:

GET /v2/schema/diff?from=v1

Returning a JSON Patch document (RFC 6902) lets clients programmatically update data models.

3.4 Version negotiation for AI agents

When autonomous agents negotiate capabilities, they often need to know which API version supports a specific protocol (e.g., a new streaming endpoint for real‑time hive telemetry). Include a Supported-Versions header in the root discovery document:

GET /  →  {
  "links": [
    { "rel": "self", "href": "/", "type": "application/json" },
    { "rel": "api-version", "href": "/v2/", "type": "application/json" }
  ],
  "supported_versions": ["v1", "v2"]
}

Agents can read this list and decide whether to upgrade or stay on an older version.


4. Pagination Techniques

4.1 Why pagination matters

Even a modest pollination dataset can explode: a single apiary reports 15 000 pollination events per season. Returning all rows in one response would inflate payload size, increase latency, and risk timeouts. Pagination caps each response, keeping average payloads under 200 KB—a sweet spot for mobile and low‑bandwidth environments.

4.2 Offset‑based pagination (the classic)

GET /v1/pollinations?limit=100&offset=200
  • Pros: Simple to implement; works with most SQL‑backed stores.
  • Cons: Unstable when new records are inserted; clients can miss or duplicate items across pages.

For static datasets (e.g., a list of endangered bee species), offset pagination is fine.

4.3 Cursor‑based pagination (recommended)

Cursor pagination uses an opaque token that encodes the last seen record’s key:

GET /v1/pollinations?limit=100&cursor=eyJpZCI6MTIzNDU2fQ==
  • Pros: Guarantees consistent ordering even as new rows appear; eliminates “skip‑scan” performance penalties on large tables.
  • Cons: Requires server‑side encoding/decoding logic; clients cannot jump to arbitrary pages (but can request the next/previous token).

Implementation tip: Use Base64‑encoded JSON that contains the primary key and a timestamp. For PostgreSQL:

SELECT * FROM pollinations
WHERE (created_at, id) > (cursor_created_at, cursor_id)
ORDER BY created_at ASC, id ASC
LIMIT 100;

4.4 Total count vs. “has‑more”

Returning the total number of items (X-Total-Count) forces the DB to run a COUNT(*), which can be expensive on large tables. A lighter alternative is a has_more boolean:

{
  "data": [...],
  "cursor": "eyJpZCI6MTIzfQ==",
  "has_more": true
}

Clients can continue paging until has_more is false. This pattern is used by the GitHub API and reduces load on the database by ~30 % for high‑traffic endpoints.

4.5 Pagination for AI agents

Autonomous agents often need to stream data rather than paginate. Offer a server‑sent events (SSE) endpoint that respects the same cursor logic:

GET /v2/pollinations/stream?cursor=eyJpZCI6MTIzfQ==
Accept: text/event-stream

Each event contains a JSON payload and a new cursor, enabling agents to keep a live, ordered feed without polling.


5. Filtering, Sorting, and Field Selection

5.1 Query parameters for filters

Standardize filter syntax to avoid ad‑hoc conventions. A common approach is field[operator]=value:

GET /v1/bees?species[eq]=Apis mellifera&status[neq]=declining

Supported operators (eq, neq, gt, lt, gte, lte, in, nin) map directly to SQL predicates, making implementation straightforward.

5.2 Sorting with explicit direction

GET /v1/bees?sort=-population,region

A leading hyphen indicates descending order. This mirrors the syntax used by Elasticsearch and the Shopify API, and it’s easy to parse server‑side.

5.3 Sparse fieldsets (partial responses)

Clients often need only a subset of fields, especially on low‑power devices. Use the fields parameter:

GET /v1/bees?fields=id,name,region

On the server, project only the requested columns. This can cut response size by up to 70 % for wide tables (e.g., a bee health record with 30 columns).

5.4 Combining with pagination

All three features—filter, sort, fields—should be composable with pagination. Example request that a front‑end dashboard might issue:

GET /v1/bees?status=healthy&sort=-last_seen&fields=id,name,last_seen&limit=50&cursor=eyJpZCI6MjM0fQ==

The server validates each parameter, applies the filter, orders the result, projects the fields, and returns a cursor for the next page.


6. HTTP Status Codes and Error Payloads

6.1 The “right” status code matters

A well‑chosen status code tells the client whether it should retry, fix the request, or abort. The most common misuses are:

Misused CodeCorrect Usage
200 OK for validation errors400 Bad Request
404 Not Found for authentication failures401 Unauthorized
500 Internal Server Error for business‑logic violations422 Unprocessable Entity

6.2 Structured error bodies

A JSON error object should contain at least:

{
  "error": {
    "code": "INVALID_PARAMETER",
    "message": "The `limit` query parameter must be an integer between 1 and 200.",
    "details": {
      "parameter": "limit",
      "provided": "abc"
    },
    "documentation_url": "https://apiary.org/docs/errors#invalid-parameter"
  }
}
  • code: Machine‑readable identifier (uppercase snake case).
  • message: Human‑readable description.
  • details: Optional map for extra context.
  • documentation_url: Direct link to the relevant section of the docs (use a slug link like [[error-codes]]).

6.3 Rate‑limit errors

When throttling, return 429 Too Many Requests with a Retry-After header (seconds or HTTP‑date). Also include a structured payload:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "You have exceeded the limit of 1,000 requests per minute.",
    "retry_after": 30
  }
}

In the Apiary ecosystem, a typical limit is 1,000 requests per minute per API key. This protects both our bee‑data pipelines and the downstream AI agents that might otherwise hammer the service.

6.4 Debug vs. production modes

Expose stack traces only in a sandbox or when a request header X-Debug: true is present. Production should never leak internal details; instead, log the full error internally and return a generic 500 Internal Server Error with a request_id:

{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred.",
    "request_id": "a1b2c3d4e5"
  }
}

Clients can report the request_id to support for faster triage.

6.5 Internationalization

If your API serves a global audience, keep the error message in English (the lingua franca of developers) but allow an optional Accept-Language header to return a localized version. Internally, store messages in a key/value store keyed by the code and language.


7. Caching and Conditional Requests

7.1 Leverage ETag and Last-Modified

For read‑only endpoints (e.g., /v1/species), include an ETag header based on a hash of the response body:

ETag: "W/\"9a8b7c6d5e4f\""

Clients can send If-None-Match: "W/\"9a8b7c6d5e4f\"" to receive a 304 Not Modified when nothing changed, saving bandwidth. For large datasets, this can reduce downstream traffic by up to 85 %.

7.2 Cache‑Control directives

  • Cache-Control: public, max-age=3600 for resources that rarely change (e.g., a static list of bee species).
  • Cache-Control: private, max-age=60 for personalized data (e.g., a user’s saved apiary locations).

Combine with Vary: Accept-Encoding, Authorization to ensure caches differentiate between authenticated and unauthenticated responses.

7.3 Stale‑while‑revalidate for AI agents

Autonomous agents that need low latency can benefit from the stale-while-revalidate directive:

Cache-Control: public, max-age=30, stale-while-revalidate=120

The client serves a cached response for up to 30 seconds, then continues to use the stale copy while a fresh version is fetched in the background. This pattern is used by CDNs serving large datasets like the Global Biodiversity Information Facility (GBIF).

7.4 Conditional DELETE

When a client wants to delete a resource only if it hasn’t changed, they can send:

DELETE /v1/bees/12345
If-Match: "W/\"9a8b7c6d5e4f\""

If the ETag mismatches, the server returns 412 Precondition Failed, preventing accidental data loss—a safety net for both human users and AI agents performing bulk clean‑ups.


8. Documentation, Hypermedia, and Discoverability

8.1 OpenAPI (Swagger) as the single source of truth

Generate an OpenAPI 3.1 spec from your codebase and host it at /openapi.json. Tools like Redoc or Swagger UI can render it automatically. Include the spec’s URL in the root response:

GET / → {
  "links": [
    { "rel": "self", "href": "/" },
    { "rel": "openapi", "href": "/openapi.json" }
  ]
}

A well‑maintained spec reduces onboarding time and enables automated client generation (e.g., openapi-generator for Python, Go, Rust).

8.2 HATEOAS for self‑governing AI agents

Hypermedia as the Engine of Application State (HATEOAS) lets clients discover actions without hard‑coding URLs. For a bee‑monitoring API, a pollination record can embed links to related resources:

{
  "id": 321,
  "timestamp": "2026-04-12T08:15:00Z",
  "links": [
    { "rel": "self", "href": "/v2/pollinations/321" },
    { "rel": "apiary", "href": "/v2/apiaries/42" },
    { "rel": "next", "href": "/v2/pollinations/322" }
  ]
}

AI agents that follow rel="next" can iterate through a dataset without knowing the pagination scheme ahead of time. This decoupling is especially useful when the API evolves (e.g., a new rel="summary" link is added in version 3).

8.3 Cross‑linking with slug

When writing documentation, reference related concepts using the [[slug]] syntax. For example:

  • “Read more about error handling in error-codes
  • “Our pagination strategy aligns with the guidelines in pagination-strategies
  • “Version negotiation is covered in detail in api-versioning

These placeholders are resolved by Apiary’s static site generator into clickable links, creating a web of knowledge that mirrors the interlinked nature of a real bee colony.

8.4 Testing and contract validation

Adopt contract‑testing tools like Pact or Dredd to verify that the live API conforms to the OpenAPI spec. Run these checks in CI/CD pipelines; a breach should block deployment. In 2023, companies that integrated contract testing saw a 23 % reduction in post‑release bugs.


9. Security, Authentication, and Rate Limiting (Brief but Essential)

While not the primary focus of this pillar, security underpins every best practice. The following concise recommendations dovetail with the earlier sections:

  1. OAuth 2.0 + JWT – Issue short‑lived access tokens (15 min) and refresh tokens (30 days). Include the token’s scope claim so endpoints can enforce fine‑grained permissions (e.g., read:pollinations vs. write:pollinations).
  1. HTTPS only – Enforce TLS 1.2+; use HSTS headers to prevent downgrade attacks.
  1. Rate limiting – Implement token bucket algorithm per API key. Return 429 with Retry-After. For AI agents that need higher throughput, provide a separate partner tier with a higher quota (e.g., 10,000 rps) after a vetting process.
  1. Input sanitization – Validate all query parameters against a whitelist. Reject unknown operators with 400 Bad Request to avoid SQL injection vectors.
  1. Audit logging – Log every request with request_id, timestamp, user ID, and endpoint. Store logs in an immutable store (e.g., AWS Glacier) for forensic analysis.

Why It Matters

A REST API is more than a set of URLs; it’s the contract that connects developers, data scientists, autonomous agents, and the ecosystems they serve. When we apply disciplined naming, versioning, pagination, and error handling, we reduce friction, improve reliability, and free up bandwidth for what truly matters—delivering timely, accurate data about bees, habitats, and the AI agents that help protect them.

Every well‑designed endpoint is a step toward a healthier planet: developers can integrate conservation data faster, AI agents can make smarter decisions with fewer round‑trips, and our community can focus on the higher‑level goal of safeguarding pollinators for generations to come. By following the practices laid out here, you’re not just building an API—you’re building a foundation for collaborative, sustainable impact.

Frequently asked
What is REST API Best Practices about?
Designing a RESTful HTTP service is a bit like tending a hive. A well‑structured API lets data flow smoothly, reduces friction for developers, and ultimately…
What should you know about 1.1 Keep URLs as nouns, not verbs?
REST treats the URL as a resource identifier —the location of a thing—while the HTTP method (GET, POST, PATCH, DELETE) describes the action. A classic anti‑pattern is /createUser or /deleteOrder . Instead, use:
What should you know about 1.2 Hierarchical nesting for relationships?
When a resource is naturally scoped under another, reflect that hierarchy in the path. For an API that tracks pollination events per apiary:
What should you know about 1.3 Use hyphens, not underscores?
Hyphens ( - ) are URL‑safe and improve readability. Browsers treat underscores ( _ ) as word separators only in some contexts, which can lead to inconsistencies in analytics. A consistent style reduces the chance of a 404 caused by a typo.
What should you know about 1.4 Avoid trailing slashes?
A trailing slash can be interpreted as a directory versus a file. Most modern frameworks treat them interchangeably, but the extra 301 redirects cost latency. Choose a convention (most APIs omit the trailing slash) and enforce it with middleware that returns a 301 Moved Permanently for mismatched requests.
References & sources
  1. Apiary Reading RoomOpen, cited knowledge base — funded to keep bee & practical research free.
From the Apiary Reading Room. Opinion & editorial — not financial advice. We don't overclaim.
More from the Reading Room