Level Up Your API Design: 8 Principles for World-Class REST APIs
Learn all about practical REST API design tips using the Richardson Maturity Model to build consistent, scalable, and easy-to-use APIs for developers.
Join the DZone community and get the full member experience.
Join For FreeYou’ve probably built a “REST API” before. But what does “RESTful” truly mean? It’s not just about using JSON and HTTP. It’s a spectrum, best described by the Richardson Maturity Model (RMM).
- Level 0 (The Swamp): Using HTTP as a transport system for remote procedure calls (RPC). Think of a single /api endpoint where all operations are POST requests.
- Level 1 (Resources): Introducing the concept of resources. Instead of one endpoint, you have multiple URIs like /users and /orders.
- Level 2 (HTTP Verbs): Using HTTP methods (GET, POST, PUT, DELETE) and status codes (2xx, 4xx) to operate on those resources. This is where most “REST” APIs live.
- Level 3 (Hypermedia — HATEOAS): The “holy grail” of REST. The API’s responses include links (hypermedia) that tell the client what they can do next. The client navigates your API by discovering these links, not by hard-coding URLs.
The eight principles I’m sharing today are a blend of my own production experience and the pragmatic wisdom from industry-leading guides like Zalando’s. These should help you move your APIs up this maturity ladder, creating designs that are more robust, scalable, and easier to use.
1. Start With “API First” Design
Before you write code, design your API contract. Using a specification like OpenAPI (a.k.a Swagger) to define endpoints, request/response models, and error codes.
This “API First” approach forces you to think about the consumer’s experience. It serves as a living document for the team and allows front-end and back-end developers to work in parallel. It is the single most important step for ensuring consistency and building developer experience.
2. Use Nouns as Resources (RMM Level 1)
Your URLs should identify resources, not actions. The action should be defined by the HTTP method (see Tip 4).
- Bad (Actions/Verbs):
- POST /api/createUser
- GET /api/getHabits
- Good (Nouns/Resources):
- POST /api/users
- GET /api/habits
The best practice here is to use plural nouns for API collection resources. It is simple, consistent, and maps cleanly to the GET /habits/{id} pattern for retrieving a single item.
3. Keep Resource URLs Simple (Avoid Nesting)
Deeply nested URLs become unwieldy and hard to manage.
- Deeply Nested (Avoid):
- /users/{userId}/habits/{habitId}/entries
This URI is already complex, and it is easy to make it worse. What if you need all entries from a habit, regardless of the user?
A flatter design is more flexible. Instead, expose a top-level resource and use query parameters for filtering:
- Flat (Prefer):
- /entries?habitId={habitId}
It is not a strict rule, but if you find yourself nesting more than one level deep (/resource/{id}/sub-resource), it’s a red flag to reconsider your design.
4. Use HTTP Verbs Correctly (RMM Level 2)
The HTTP method is your verb. Let it do the work.
- GET: Retrieve a resource or collection. (Safe and idempotent)
- POST: Create a new resource. (Not idempotent)
- PUT: Update a resource (replaces the entire resource). (Idempotent)
- DELETE: Remove a resource. (Idempotent)
- PATCH: Partially update a resource. (Often skipped for simplicity, but it’s the “correct” verb for partial updates).
Using these verbs correctly and consistently is the core of a Level 2 REST API.
5. Return Meaningful HTTP Status Codes (RMM Level 2)
Don’t just return 200 OK for everything. The status code is a critical part of the contract.
- 2xx (Successful):
- 200 OK: Standard success for GET.
- 201 Created: The POST was successful, and a new resource was created. The response should include a Location header pointing to the new resource.
- 204 No Content: The operation (like a DELETE) was successful, and there’s nothing to return to the body.
- 4xx (Client Errors):
- 400 Bad Request: A generic error for malformed input or a business logic failure (e.g., “Not enough stock”).
- 401 Unauthorized: The user isn’t authenticated.
- 403 Forbidden: The user is authenticated, but doesn’t have permission for this action.
- 404 Not Found: The requested resource doesn’t exist.
- 5xx (Server Errors):
- 500 Internal Server Error: Your code crashed. The client can’t fix this.
6. Standardize Your Error Format
This is non-negotiable for a good API. When a 4xx or 5xx error occurs, your consumers shouldn’t have to guess the response format.
Stop inventing your own error objects. Use the industry standard: Problem Details (RFC 7807).
It’s a standardized JSON format (content type application/problem+json) that defines fields for:
- Type: A URI pointing to documentation about this error.
- Title: A short, human-readable summary.
- Status: The HTTP status code.
- Detail: A more specific explanation.
- Instance: The URI of the resource that had the problem.
Almost all modern web frameworks have built-in support for Problem Details. Use it.
7. Use Envelopes, Pagination, and Hypermedia (RMM Level 3)
This tip combines three ideas that unlock the full power of your API, moving you toward RMM Level 3.
First, wrap your collection responses in an “envelope.”
- Bad (Naked Array):
JSON
{ "id": 1, "name": "Entry 1" } - Good:
JSON
{ "data": [ { "id": 1, "name": "Entry 1" } ] }
8. Be Pragmatic and Consistent
Be consistent.
This is the most important rule:
- If you use plural nouns, use them everywhere.
- If you use
400 Bad Requestfor validation, use it everywhere for validation.
But also, be pragmatic. You may read that "true REST" (Level 3) is the only way. But Level 3 adds complexity for both the server and the client. Most of the world's best APIs live happily at Level 2, with some elements of Level 3 (like pagination links).
Use these principles. Scrutinize them for your context. And whatever you decide, apply it consistently.
An Example to Summarize
Let us walk through how this looks in a real application. This applies to any application, but for this case, we are assuming a "Habit Tracker" application. The example shows a RESTful API for managing personal habits, built on these principles.
EntriesController (an ASP.NET Core example), you can see:
- API first (via attributes): I would define all response types (
[ProducesResponseType]) and content types, which feed directly into the OpenAPI documentation. - Nouns and verbs: The route should be a plural noun
[Route("entries")], and the methods are HTTP verbs like[HttpGet],[HttpPost]and so on. - Status codes: The
POSTendpoint returns201 Createdwith the correctLocationheader. TheGET(single) endpoint returns404 Not Foundor200 OK. - Envelopes and pagination: The collection
GETendpoint returns aPaginationResultobject (the envelope) and includes logic to generate hypermedia links. - Error handling: The framework is configured to automatically return
application/problem+jsonfor all4xx/5xxerrors.
This practical, consistent application of these rules leads to an API that is predictable, scalable, and a pleasure for other developers to consume.
Conclusion
Good API design is not just about ticking boxes on the Richardson Maturity Model (RMM); it is about creating an easily understandable and consumable API for the developers who rely on your code. Whether you aim for the full discoverability of Level 3 hypermedia or settle into a robust, consistent Level 2 workflow, the ultimate metric of success is usability.
Combining these structural best practices with a mindset of consistency and pragmatism will let you transform your API from a mere data pipe into a durable, intuitive product. Start small, stick to your contract, and build an interface that you would actually want to use yourself.
Opinions expressed by DZone contributors are their own.
Comments