Skip to main content
Back to Blog
RESTAPI DesignWeb DevelopmentBackendPaginationVersioningBest PracticesSoftware Architecture

Designing RESTful APIs: Naming, Versioning, and Pagination Done Right

A comprehensive guide to designing clean, scalable, and maintainable RESTful APIs — covering resource naming conventions, versioning strategies, and pagination patterns with real-world examples and modern best practices.

May 15, 202615 min readNiraj Kumar

APIs are contracts. Every endpoint you expose, every field you name, and every versioning decision you make becomes a promise to every developer who integrates with your system. Break that promise carelessly, and you create cascading failures across clients, mobile apps, and third-party integrations. Honor it thoughtfully, and you build a platform people want to build on.

This guide is a deep-dive into three pillars of excellent REST API design — naming conventions, versioning strategies, and pagination patterns — grounded in real-world examples, industry standards (including OpenAPI 3.1 and JSON:API), and hard-won lessons from production systems. Whether you're building your first API or refactoring a legacy monolith's surface, you'll find concrete, actionable guidance here.


Why API Design Matters More Than Ever

In 2026, APIs are no longer internal plumbing — they are products. The rise of micro-frontend architectures, edge computing, and AI-driven integrations means your API surface is consumed by more clients, in more contexts, than ever before.

Poor API design compounds over time. A confusing naming scheme forces every consumer to build mental translation layers. An unversioned API that changes without warning breaks mobile apps users can't force-update. An offset-based pagination scheme that made sense at 10,000 records becomes a performance nightmare at 10 million.

Good design, on the other hand, is a compounding investment. Let's build it right from the start.


Part 1: Resource Naming — The Foundation of a Readable API

Think in Nouns, Not Verbs

The single most common beginner mistake in REST API design is using verbs in endpoint paths. REST is a resource-oriented architecture. HTTP verbs (GET, POST, PUT, PATCH, DELETE) already carry the action — your URI should describe the thing, not the operation.

❌ Avoid:

GET  /getUsers
POST /createUser
PUT  /updateUserProfile
DELETE /deleteUser/42
GET  /fetchOrdersByCustomer

✅ Prefer:

GET    /users
POST   /users
PUT    /users/{id}/profile
DELETE /users/{id}
GET    /users/{id}/orders

This shift in mental model — from "what am I doing" to "what am I touching" — unlocks the full expressive power of HTTP verbs and makes your API immediately intuitive.

Use Plural Nouns for Collections

Collections should always be plural. Singular naming creates ambiguity: does /user mean "the currently authenticated user" or "any user"? Plural makes the intent unambiguous.

/users          → collection of users
/users/{id}     → a specific user
/products       → collection of products
/products/{id}  → a specific product

Lowercase and Hyphenated, Always

URIs are case-sensitive by specification (RFC 3986). Using camelCase or PascalCase in paths creates a trap: /userProfiles and /userprofiles are different URIs. Stick to lowercase throughout and use hyphens (not underscores) for readability.

❌ Avoid:

/userProfiles
/user_profiles
/UserProfiles

✅ Prefer:

/user-profiles

Hyphens are preferred over underscores because some fonts and displays render underscores beneath link underline decorations, making them invisible or confusing.

Model Relationships with Nested Resources (Carefully)

Nested resources are a natural way to express ownership or containment. An order belongs to a user; a comment belongs to a post.

GET /users/{userId}/orders           → all orders for a user
GET /users/{userId}/orders/{orderId} → a specific order for a user
GET /posts/{postId}/comments         → all comments on a post

However, nesting deeper than two or three levels quickly becomes unwieldy and couples your URL structure too tightly to your data model. If you need /organizations/{orgId}/teams/{teamId}/members/{memberId}/permissions, it's time to reconsider.

Practical rule: Nest when the child resource only makes sense in the context of its parent. If a resource can stand alone (e.g., a comment might be retrieved by its ID globally), give it a top-level route in addition to the nested one.

GET /comments/{commentId}   → also valid — direct access by ID

Query Parameters for Filtering, Sorting, and Searching

Filtering and sorting are not part of the resource path — they are modifiers on a collection query. Use query parameters for these.

GET /products?category=electronics&maxPrice=500
GET /users?status=active&sortBy=createdAt&sortOrder=desc
GET /orders?fromDate=2026-01-01&toDate=2026-03-31
GET /products?q=wireless+headphones   ← full-text search

Define consistent parameter naming conventions across your entire API. If one endpoint uses sort_by and another uses sortBy, you've introduced friction for every client developer.


Part 2: Versioning — Making Change Safe

Every API will change. New fields get added, old ones become deprecated, business logic evolves. The question isn't whether you'll need to version your API — it's how you'll do it without breaking your consumers.

The Four Major Versioning Strategies

1. URI Path Versioning (Most Common)

https://api.yourservice.com/v1/users
https://api.yourservice.com/v2/users

Pros:

  • Immediately visible and bookmarkable
  • Easy to test in a browser
  • Simple to route at the infrastructure level (nginx, API gateway)
  • Most intuitive for developers browsing documentation

Cons:

  • Technically violates REST's principle that a URI should identify a single resource (v1 and v2 of the same resource are different URIs)
  • Can lead to URI proliferation over many major versions

Best for: Public APIs, consumer-facing products, any API where discoverability and simplicity are paramount.

2. Header-Based Versioning

GET /users HTTP/1.1
Host: api.yourservice.com
API-Version: 2026-05-01

Or using the Accept header (content negotiation):

GET /users HTTP/1.1
Accept: application/vnd.yourservice.v2+json

Pros:

  • Keeps URIs clean and stable
  • Aligns with HTTP's content negotiation model
  • Scales gracefully for granular version control

Cons:

  • Not visible in browser address bar
  • Harder to test without tooling (curl, Postman, etc.)
  • Can be confusing for new integrators

Best for: Internal APIs, partner integrations, APIs consumed primarily by sophisticated clients.

3. Date-Based Versioning (Stripe Model)

Stripe popularized a date-stamped versioning approach that's worth studying:

GET /v1/customers HTTP/1.1
Stripe-Version: 2024-11-20

Each API key is pinned to the version in use when first created. Developers explicitly opt into new versions, and Stripe maintains a changelog per version date.

Pros:

  • Extremely fine-grained control
  • Minimizes breaking change surface area
  • Encourages explicit version upgrades

Cons:

  • Complex to implement and document
  • Requires meticulous version management infrastructure

Best for: High-stakes, mission-critical APIs with many long-term integrators (payment systems, ERP connectors).

4. Query Parameter Versioning

GET /users?version=2

Generally discouraged — it conflates routing metadata with resource filtering. Use this only as a last resort or for quick prototyping.

When to Bump a Version (And When Not To)

Not every change requires a major version bump. Understand the difference between additive and breaking changes.

Non-breaking (safe to deploy without versioning):

  • Adding new optional fields to response bodies
  • Adding new optional request parameters
  • Adding new endpoints
  • Adding new enum values (be cautious here — some serializers fail on unknown values)
  • Performance improvements that don't change behavior

Breaking (requires a new version):

  • Removing fields from response bodies
  • Renaming fields or endpoints
  • Changing field types (e.g., id from integer to UUID)
  • Changing authentication schemes
  • Altering the shape of error responses
  • Removing enum values
// v1 response — id is an integer
{
  "id": 42,
  "name": "Ada Lovelace"
}

// v2 response — id migrated to UUID (BREAKING CHANGE)
{
  "id": "usr_01hw4b7k2v3j4m6n7p8q",
  "name": "Ada Lovelace"
}

Deprecation Done Right

Versioning isn't just about introducing new versions — it's about sunsetting old ones gracefully. Use the Deprecation and Sunset response headers (RFC 8594) to communicate timelines:

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: <https://api.yourservice.com/v2/users>; rel="successor-version"

This gives consumers months of warning, machine-readable sunset dates, and a direct link to the replacement. Always pair deprecation headers with a clear entry in your changelog and a developer-facing migration guide.


Part 3: Pagination — Scaling Your Collections Gracefully

Every collection endpoint will eventually outgrow the "return everything" approach. Pagination isn't an optimization — it's a fundamental requirement for any API that handles non-trivial data volumes.

The Three Core Pagination Strategies

1. Offset Pagination (Page-Based)

The most familiar pattern: skip N items and return the next M.

GET /products?page=3&limit=20
GET /products?offset=40&limit=20

Response envelope:

{
  "data": [...],
  "pagination": {
    "total": 1847,
    "page": 3,
    "limit": 20,
    "totalPages": 93,
    "hasNextPage": true,
    "hasPreviousPage": true
  }
}

Pros:

  • Intuitive for users and developers
  • Allows jumping to arbitrary pages
  • Simple to implement with SQL LIMIT/OFFSET

Cons:

  • Page drift: If items are inserted or deleted between requests, pages shift. A user on page 3 might skip or see duplicate items.
  • Performance: OFFSET 10000 in SQL requires the database to scan and discard 10,000 rows.
  • No absolute consistency for real-time datasets.

Best for: Admin dashboards, search results, small static datasets where real-time consistency isn't critical.

Instead of a numeric offset, you use an opaque cursor — typically a Base64-encoded pointer to the last item seen.

GET /posts?limit=20&cursor=eyJpZCI6MTAwfQ==

Response envelope:

{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6MTIwfQ==",
    "previousCursor": "eyJpZCI6MTAxfQ==",
    "hasNextPage": true,
    "hasPreviousPage": true,
    "limit": 20
  }
}

Internally, the cursor decodes to something like {"id": 100, "createdAt": "2026-05-10T08:00:00Z"}, and your query becomes:

SELECT * FROM posts
WHERE created_at < '2026-05-10T08:00:00Z'
   OR (created_at = '2026-05-10T08:00:00Z' AND id < 100)
ORDER BY created_at DESC, id DESC
LIMIT 21;  -- fetch one extra to determine hasNextPage

Pros:

  • Stable pagination even with concurrent inserts/deletes
  • Constant-time performance regardless of depth
  • Natural fit for infinite scroll and real-time feeds

Cons:

  • Cannot jump to an arbitrary page
  • Cursors must be kept opaque (Base64 or encrypted) to prevent client manipulation
  • More complex implementation

Best for: Social feeds, activity logs, any high-volume, real-time collection.

3. Keyset Pagination

A close cousin of cursor-based pagination that uses explicit, visible key values rather than opaque tokens:

GET /users?afterId=10042&limit=25

Simpler to implement, but exposes your internal IDs and provides no security against cursor manipulation. Good for internal tools and developer-friendly APIs where transparency is preferred over opacity.

Standardizing Your Pagination Envelope

Pick one response shape and use it everywhere. Inconsistency across endpoints is one of the most common developer pain points.

{
  "data": [
    { "id": "usr_01hw", "name": "Ada Lovelace" },
    { "id": "usr_02xk", "name": "Grace Hopper" }
  ],
  "meta": {
    "total": 5823,
    "limit": 25,
    "cursor": {
      "next": "eyJpZCI6InVzcl8wMnh" ,
      "previous": null
    }
  },
  "links": {
    "self": "/users?limit=25",
    "next": "/users?limit=25&cursor=eyJpZCI6InVzcl8wMnh",
    "previous": null
  }
}

Including links with pre-built URLs follows HATEOAS (Hypermedia as the Engine of Application State) principles and means client developers never have to manually construct pagination URLs.


Part 4: Error Handling — The Underrated Design Surface

Well-designed errors are as important as well-designed success responses. A cryptic 500 Internal Server Error forces developers to guess; a structured error object tells them exactly what went wrong and how to fix it.

Adopt a Consistent Error Shape

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request body contains invalid data.",
    "details": [
      {
        "field": "email",
        "issue": "INVALID_FORMAT",
        "message": "Must be a valid email address."
      },
      {
        "field": "age",
        "issue": "OUT_OF_RANGE",
        "message": "Must be between 18 and 120."
      }
    ],
    "requestId": "req_01hw4b7k2v",
    "documentationUrl": "https://docs.yourservice.com/errors/VALIDATION_ERROR"
  }
}

Use HTTP Status Codes Semantically

Status CodeMeaningWhen to Use
200 OKSuccessSuccessful GET, PUT, PATCH
201 CreatedResource createdSuccessful POST that creates a resource
204 No ContentSuccess, no bodySuccessful DELETE
400 Bad RequestClient errorInvalid input, malformed JSON
401 UnauthorizedNot authenticatedMissing or invalid auth token
403 ForbiddenNot authorizedAuthenticated but lacks permission
404 Not FoundResource not foundNon-existent ID
409 ConflictState conflictDuplicate email, optimistic lock failure
422 Unprocessable EntitySemantic validation failureStructurally valid but logically invalid data
429 Too Many RequestsRate limitedInclude Retry-After header
500 Internal Server ErrorServer-side bugNever expose stack traces

Part 5: Common Mistakes to Avoid

❌ Returning 200 for Errors

// WRONG — status is 200 but this is clearly an error
HTTP/1.1 200 OK
{ "success": false, "error": "User not found" }

Use proper status codes. Clients parse status codes first — burying error signals in the body body breaks every HTTP client library's built-in error handling.

❌ Ignoring Idempotency

PUT and DELETE must be idempotent: calling them multiple times should produce the same result. POST is not idempotent by default. If you need idempotent POST (e.g., for payment submissions), implement idempotency keys:

POST /payments HTTP/1.1
Idempotency-Key: idem_01hw4b7k2v3j4m6n7p8q

❌ Leaking Internal Implementation Details

Your API should be a stable abstraction. Never:

  • Expose database primary keys when UUIDs/slugs are more stable
  • Expose table names or internal service names in error messages
  • Use internal enum strings that could change with a refactor

❌ Inconsistent Date Formats

Always use ISO 8601 with UTC timezone across every date field:

// ✅ Correct
"createdAt": "2026-05-15T08:30:00Z"

// ❌ Avoid
"createdAt": "May 15, 2026"
"createdAt": 1747296600   // Unix timestamp (hard to read)
"createdAt": "05/15/2026" // Ambiguous locale-dependent format

❌ Not Documenting Your API

In 2026, an undocumented API is a liability. Use OpenAPI 3.1 to generate interactive documentation automatically. Tools like Swagger UI, Redoc, and Scalar can serve your spec as a beautiful developer portal with zero additional effort.

openapi: 3.1.0
info:
  title: Your Service API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Paginated list of users

🚀 Pro Tips

Tip 1: Design for the Failing Case First

Before you write a single line of handler code, design your error responses. Work backwards from failure: what can go wrong? What should the client do about it? This discipline produces more robust, predictable APIs.

Tip 2: Use Semantic Prefixes for IDs

Instead of opaque UUIDs like a1b2c3d4-..., use prefixed IDs (à la Stripe):

usr_01hw4b7k2v   → clearly a user
ord_02xk9m3n4p   → clearly an order
prd_03yl0n5q6r   → clearly a product

This makes debugging and log analysis dramatically faster — you know the type of resource just from glancing at an ID.

Tip 3: Implement PATCH with JSON Merge Patch

Avoid the antipattern of PUT-ing an entire resource just to change one field. Implement PATCH using JSON Merge Patch (RFC 7396):

PATCH /users/usr_01hw HTTP/1.1
Content-Type: application/merge-patch+json

{ "email": "new@example.com" }

Only the provided fields are updated; omitted fields remain unchanged.

Tip 4: Add ETag Headers for Caching and Conflict Detection

HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e"
Cache-Control: max-age=3600

Clients can use If-None-Match on subsequent requests — if the resource hasn't changed, you return 304 Not Modified and save bandwidth. ETags also enable optimistic concurrency control for PUT and PATCH operations.

Tip 5: Rate Limit Headers Are Your Friend

When you rate-limit requests (and you should), communicate the limits in response headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1747300200
Retry-After: 3600

This gives clients everything they need to implement smart retry logic without hammering your API.


📌 Key Takeaways

  • Naming is communication. Use lowercase, plural, hyphenated nouns for resource paths. Let HTTP verbs carry the action, and keep nesting shallow. Consistency beats cleverness every time.

  • Versioning is a long-term commitment. URI versioning is the pragmatic default for public APIs. Adopt header or date-based versioning for fine-grained control. Plan your deprecation lifecycle before you launch v1.

  • Choose pagination by your data's character. Cursor-based pagination is the production standard for real-time, high-volume collections. Offset pagination is fine for small, static datasets. Never return unbounded collections.

  • Errors are part of your API surface. Structured, consistent error responses with proper HTTP status codes, machine-readable error codes, and documentation links transform developer experience from frustrating to productive.


Putting It All Together: A Reference Endpoint Design

Here's what a well-designed collection endpoint looks like when all these principles converge:

GET /v1/orders?status=shipped&limit=25&cursor=eyJpZCI6MTAwfQ==
Authorization: Bearer eyJhbGci...
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "7a9f3c2d8e1b"
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 982
X-RateLimit-Reset: 1747300200
Deprecation: false
{
  "data": [
    {
      "id": "ord_03yl0n5q6r",
      "status": "shipped",
      "total": {
        "amount": 12999,
        "currency": "USD"
      },
      "createdAt": "2026-05-14T10:22:00Z",
      "updatedAt": "2026-05-15T03:45:00Z"
    }
  ],
  "meta": {
    "total": 1483,
    "limit": 25,
    "cursor": {
      "next": "eyJpZCI6MTI1fQ==",
      "previous": null
    }
  },
  "links": {
    "self": "/v1/orders?status=shipped&limit=25&cursor=eyJpZCI6MTAwfQ==",
    "next": "/v1/orders?status=shipped&limit=25&cursor=eyJpZCI6MTI1fQ==",
    "previous": null
  }
}

Clean, self-describing, scalable, and ready for production.


Conclusion

Designing a great REST API is fundamentally an act of empathy — for the developers who will consume it, the systems that will extend it, and the future you who will have to maintain it. The conventions we've covered — consistent naming, thoughtful versioning, appropriate pagination, and structured error handling — aren't arbitrary rules. They're the accumulated wisdom of the industry, refined through years of building, breaking, and fixing real-world systems.

Start with these foundations, document them in an OpenAPI spec from day one, and treat every endpoint decision as an architectural choice with long-term implications. The APIs that stand the test of time aren't the cleverest ones — they're the most predictable, the most consistent, and the most considerate.

Build APIs you'd be proud to integrate with.


References

All Articles
RESTAPI DesignWeb DevelopmentBackendPaginationVersioningBest PracticesSoftware Architecture

Written by

Niraj Kumar

Software Developer — building scalable systems for businesses.