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.,
idfrom 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 10000in 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.
2. Cursor-Based Pagination (Recommended for Production)
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 Code | Meaning | When to Use |
|---|---|---|
200 OK | Success | Successful GET, PUT, PATCH |
201 Created | Resource created | Successful POST that creates a resource |
204 No Content | Success, no body | Successful DELETE |
400 Bad Request | Client error | Invalid input, malformed JSON |
401 Unauthorized | Not authenticated | Missing or invalid auth token |
403 Forbidden | Not authorized | Authenticated but lacks permission |
404 Not Found | Resource not found | Non-existent ID |
409 Conflict | State conflict | Duplicate email, optimistic lock failure |
422 Unprocessable Entity | Semantic validation failure | Structurally valid but logically invalid data |
429 Too Many Requests | Rate limited | Include Retry-After header |
500 Internal Server Error | Server-side bug | Never 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
- RFC 7231 — HTTP/1.1 Semantics and Content
- RFC 7396 — JSON Merge Patch
- RFC 8594 — The Sunset HTTP Header Field
- RFC 3986 — Uniform Resource Identifier (URI): Generic Syntax
- OpenAPI Specification 3.1.0
- JSON:API Specification
- Stripe API Reference — Widely regarded as the gold standard for developer-friendly API design
- Google API Design Guide
- Microsoft REST API Guidelines
- Zalando RESTful API Guidelines