Who This Guide Is For
This guide is for backend developers designing or restructuring APIs that other developers will consume — whether those developers are on your own frontend team, building mobile applications, or integrating from external systems. You understand HTTP and have built endpoints before, but want the design conventions that make an API predictable, consistent, and maintainable as it grows from a handful of endpoints to hundreds. This guide covers the structural decisions you make once and live with for years.
Before You Start
You should have a clear understanding of the domain your API serves and the clients that will consume it. An API for a single-page application has different requirements than an API for third-party integrations. The former can change frequently because you control both sides. The latter demands stability because breaking changes affect people you do not employ. Know which you are building before making structural decisions. If you are building for both, the third-party constraints should drive your design.
Step 1: Design Resources Around Domain Concepts
REST APIs are organised around resources — the nouns of your domain. A well-designed resource model maps to business concepts that are stable over time, not to database tables or UI screens that change with implementation details.
Use plural nouns for collection endpoints. Resources represent collections by default: /projects, /invoices, /users. Individual resources are accessed by their identifier: /projects/42, /invoices/abc123. This convention is universal enough that deviating from it creates friction for every developer who consumes your API.
Nest resources to express relationships, but only one level deep. /projects/42/tasks is clear: tasks that belong to project 42. /projects/42/tasks/17/comments/3/attachments is too deep — it creates rigid URL structures that are difficult to maintain and painful to construct. For deeper relationships, promote the nested resource to a top-level resource and use query parameters for filtering: /attachments?comment_id=3.
Use consistent naming conventions. Choose between kebab-case (project-milestones) and snake_case (project_milestones) for multi-word resources and apply the choice everywhere. Mix-and-match naming is the single most common source of API inconsistency. For JSON request and response bodies, snake_case is the most widely used convention and maps cleanly to most programming languages.
Actions that do not fit CRUD. Some operations do not map naturally to creating, reading, updating, or deleting a resource. Sending an invoice, archiving a project, or running a report are actions, not resource manipulations. Two approaches work: model the action as a sub-resource (POST /invoices/42/send) or as a dedicated action endpoint (POST /projects/42/archive). The sub-resource approach is more RESTful. The action endpoint approach is more explicit. Both are acceptable — the important thing is to pick one convention and apply it consistently.
Avoid verbs in URLs. The HTTP method (GET, POST, PUT, PATCH, DELETE) provides the verb. GET /projects retrieves projects. POST /projects creates one. PUT /projects/42 replaces one. PATCH /projects/42 partially updates one. DELETE /projects/42 removes one. URLs like /getProjects or /createProject duplicate the method in the URL and create ambiguity about what the HTTP method should be.
Step 2: Implement Versioning From the Start
API versioning is cheap to implement at the beginning and expensive to retrofit later. Even if you think your API will never need a breaking change, version it. Requirements change, mistakes are discovered, and the cost of having a version prefix in your URLs is zero.
URL-based versioning is the simplest and most visible approach: /api/v1/projects. The version is part of the URL, immediately visible in logs, documentation, and developer tools. When you need to introduce a breaking change, stand up /api/v2/ endpoints alongside v1. Clients migrate on their schedule.
Header-based versioning uses a custom header (Accept: application/vnd.yourapi.v1+json) or query parameter (?version=1) to specify the version. This keeps URLs clean but hides the version from casual inspection. It also requires more sophisticated routing in your application. Unless you have a specific reason to prefer header-based versioning, URL-based is the better default.
What constitutes a breaking change: removing a field from a response, renaming a field, changing a field’s type, removing an endpoint, changing the meaning of a parameter, or changing an error response format. These all break existing clients and require a new version. Adding a new field, adding a new endpoint, or adding a new optional parameter are non-breaking and can be deployed to the existing version.
Deprecation policy. When you release a new version, the old version must remain available for a defined period — ninety days is a reasonable minimum for external APIs. Include a Sunset header in responses from deprecated versions with the date they will be removed. Log usage of deprecated versions so you know when it is safe to remove them.
Do not version prematurely. Version 1 should remain version 1 for as long as possible. Adding non-breaking changes to v1 is always preferable to creating v2 for minor improvements. A proliferation of versions creates maintenance overhead and confuses clients about which version to use.
Step 3: Standardise Error Responses
Error responses are the part of your API that developers interact with most during development and debugging. A consistent, informative error format reduces integration time and support requests.
Use HTTP status codes correctly. 200 for success. 201 for resource creation. 204 for successful deletion with no response body. 400 for client errors (bad input). 401 for unauthenticated requests. 403 for authenticated but unauthorised requests. 404 for resources that do not exist. 422 for validation errors (input is syntactically correct but semantically invalid). 429 for rate limiting. 500 for server errors. Do not use 200 for everything and encode the real status in the body — this breaks HTTP semantics and confuses client libraries, caches, and monitoring tools.
Standardise the error body. Every error response should have the same shape. A minimal structure includes: a machine-readable error code or type, a human-readable message, and (for validation errors) a field-level breakdown. The machine-readable code allows client code to handle specific errors programmatically. The human-readable message helps developers debug during integration.
Validation errors need field-level detail. A 422 response for a form submission should indicate which fields failed and why: “email: must be a valid email address, name: is required.” Returning only “Validation failed” forces the client developer to guess which field is wrong. Map validation errors to field names so the client can display inline error messages.
Do not expose internal details in production. Stack traces, SQL queries, file paths, and class names belong in development and staging error responses, not in production. Production error responses should contain enough information for the client to understand and correct the problem, but nothing that helps an attacker understand your system’s internals.
Include a request identifier. Generate a unique identifier for every request and include it in the response (both success and error). When a client reports a problem, the request ID allows your team to find the corresponding log entry immediately. Without it, debugging client-reported issues requires timestamp-based log searching that is slow and imprecise.
Step 4: Implement Pagination, Filtering, and Sorting
Any endpoint that returns a collection will eventually return too many items to fit in a single response. Pagination is not optional for production APIs — it is a requirement from day one.
Cursor-based pagination is the most reliable approach for APIs with frequently changing data. The client sends a cursor (an opaque string that identifies a position in the result set), and the server returns the next page of results along with the cursor for the subsequent page. Cursor pagination does not have the consistency problems of offset pagination (where inserting or deleting items causes items to be skipped or duplicated across pages) and performs well on large datasets because the database query uses an indexed column rather than an OFFSET.
Offset-based pagination is simpler to implement and easier for clients to understand: page=3&per_page=25 returns items 51 through 75. The trade-off is that it performs poorly on large datasets (the database must count and skip rows) and is inconsistent when data changes between page requests. For APIs with relatively stable data and moderate collection sizes, offset pagination is acceptable.
Always include pagination metadata. The response should include the current page or cursor, the number of items per page, the total count (if feasible to calculate), and links or cursors for the next and previous pages. Clients should never need to guess whether more data exists.
Filtering and sorting allow clients to request specific subsets of data. Use query parameters for both: /projects?status=active&sort=-created_at (where the minus prefix indicates descending order). Document which fields support filtering and sorting. Do not allow filtering or sorting on unindexed database columns — this creates performance problems that worsen as data grows.
Set reasonable defaults. If a client does not specify pagination parameters, return a default page size (25 is common) rather than the entire collection. If a client requests an unreasonably large page size, cap it at a maximum (100 or 200). These defaults protect your API from accidental or intentional resource exhaustion.
Step 5: Secure and Authenticate
Authentication and authorisation are structural concerns that affect every endpoint. Get them right at the beginning because changing your authentication scheme after clients have integrated is a breaking change.
Token-based authentication is the standard for APIs. The client includes a Bearer token in the Authorization header of every request. The server validates the token and identifies the user. Tokens can be opaque (looked up in a database), self-contained (JWTs), or managed by a framework (Laravel Sanctum, Passport). For first-party clients (your own frontend and mobile apps), session-based tokens or Sanctum tokens are appropriate. For third-party integrations, API keys or OAuth2 client credentials are more suitable.
Authenticate every request independently. APIs should be stateless — each request contains all the information needed to authenticate and authorise it. Do not rely on server-side sessions for API authentication. Sessions introduce state that complicates scaling, caching, and load balancing.
Authorise at the resource level. Authentication confirms who the user is. Authorisation confirms what they can do. After authenticating a request, check that the user has permission to perform the requested operation on the requested resource. A user who is authenticated to access /projects should only see their own projects, not all projects in the system.
Rate limit by authentication identity. Authenticated requests should be rate limited by user identity. Unauthenticated requests should be rate limited by IP address. Include rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) in every response so clients can manage their request budget. Return 429 when limits are exceeded with a Retry-After header.
Use HTTPS exclusively. API tokens sent over unencrypted HTTP are visible to anyone on the network. There is no legitimate reason to serve an API over HTTP in production. Redirect HTTP requests to HTTPS and set HSTS headers to prevent downgrade attacks.
Common Mistakes
- Inconsistent naming. Mixing camelCase, snake_case, and kebab-case across endpoints and response bodies creates friction for every client. Choose one convention and enforce it.
- No versioning. The first time you need a breaking change and have no version prefix, you face a choice between breaking all clients or maintaining two URL schemes. Version from the start.
- Generic error messages. “Something went wrong” does not help a developer debug an integration. Include specific error codes, field-level validation detail, and request identifiers.
- No pagination on collections. An endpoint that returns all records works fine with ten items and takes down your server with ten million. Paginate from day one.
- Exposing database internals. Auto-increment IDs leak information about your data volume and creation order. Internal field names that differ from your public API contract create confusion. Design your API surface independently of your database schema.
What Good Looks Like
A well-structured REST API has: resource-oriented URLs with consistent naming, versioning from the first release, standard HTTP status codes with structured error bodies that include request identifiers, pagination on every collection endpoint with metadata and sane defaults, token-based authentication with per-user rate limiting, and documentation that is accurate and current. The API is predictable enough that a developer who has used one endpoint can guess the shape of another, and stable enough that integrations built against it today still work a year from now.
Next Steps
For securing the API endpoints you have designed, How to Implement Rate Limiting covers the patterns that protect your API from abuse. For the payment endpoints that many APIs include, How to Integrate Stripe Payments covers the specific patterns for billing integration.