Mastering REST: Best Practices and Lessons from the Real World
REST APIs are everywhere in modern development, but are you building them the right way?
It’s easy to think there are just a few simple rules, but in the real world, we often face tricky scenarios and tough decisions. This post will guide you through common pitfalls and best practices, turning you into a more confident and effective API developer.
We’ll break down insights from a deep-dive session with Thiago, co-author of the book Mastering RESTful Web Services with Java: Practical guide for building secure and scalable production-ready REST APIs, to help you avoid mistakes and build robust, easy-to-use APIs.
The Starting Point: A Problematic API #
As a start we were looking at a sample piece of code. Thiago asked what the audience thinks could be improved with it.
During the discussion, we then pointed out several problems with this approach.
- Wrong HTTP Method: As Csilla noted, “if you are getting some elements, a list, then you are using a
GETand not aPOSTmethod.” UsingPOSTto retrieve data is confusing and goes against REST principles. - Verbs in the URL: The path contained verbs. RESTful URLs should represent resources, not actions.
- Singular Noun for a Collection: The resource was named in singular but it returned a list. It’s a convention to use plural nouns for collections, like
/users. - Exposing the Internal Entity: The method returned a
List<User>, whereUseris likely a database entity. This is risky because you might accidentally expose sensitive data, like passwords or internal fields.
Thiago shared a real-world example of how even popular projects can get this wrong.
Thiago: “There is a big application called mock server… if you look inside the mock server application, all the endpoints use the
PUTmethod. They have only one single verb that it’sPUTand also have verbs in the endpoints. So the API is not so clear as it should be.”
This often happens when developers are just trying to get things working quickly. Yugo remembered this from his own experience.
Yugo: “I remember implementing lots of controllers with these endpoints, most of them
POSTbecause it was easier to create a post where you have to send an object and then handle it on the endpoint. And then came the idea of resources, which is what REST suggests.”
Building Better APIs: The Best Practices #
Now that we’ve seen what not to do, let’s look at how to build clean, predictable, and maintainable REST APIs.
Choose the Right HTTP Methods and Status Codes #
Using the correct HTTP method is fundamental. Each method has a specific purpose. Combine them with the right HTTP status codes to give clear feedback to the client.
| Method | Purpose | Common Success Codes |
|---|---|---|
GET |
Retrieve a resource or a collection. | 200 (OK) |
POST |
Create a new resource. | 201 (Created), 202 (Accepted, If async processing) |
PUT |
Replace an entire existing resource. | 200 (OK), 204 (No Content) |
PATCH |
Apply a partial update to a resource. | 200 (OK), 204 (No Content) |
DELETE |
Delete a resource. | 204 (Deleted immediately), 202 (Accepted, If async processing) |
Nail Your Naming Conventions #
Consistency is king! A clear and consistent naming scheme makes your API predictable and easy for other developers to use.
- Use nouns, not verbs: Use
/users, not/getUsers. - Use plural nouns: Prefer
/usersover/userfor collections. You can use/users/{id}to access a specific user. - Use lowercase letters: Keep your URLs simple, like
/users, not/Users. - Separate words with hyphens: If you need to separate words, use a hyphen, like
/purchase-orders.
Don’t Expose Your Database Entities! #
Remember the problem of returning the User entity directly? The solution is to use Data Transfer Objects (DTOs). A DTO is a simple class that defines exactly what data your API will send or receive.
This decouples your API from your internal database structure. You can have a UserRequest DTO for creating a user and a UserResponse DTO for returning user data, making sure you only expose the fields you want.
Thiago: “We can create a DTO… We should decouple the responsibilities. The best thing is to have a class for the request and the response, and then we can transform this data to our domain.”
To map between your entities and DTOs without writing a lot of boilerplate code, you can use a library like MapStruct.
Thiago: “There is a very easy way that is using MapStruct, which is a library that you can use that basically generates an implementation for this interface that does this kind of mapping.”
The API-First Approach with OpenAPI #
For even better results, consider an API-first approach using the OpenAPI Specification. With this approach, you first define your API in a YAML or JSON file. This file becomes your single source of truth.
This contract allows frontend and backend teams to work in parallel. It also enables powerful tools. For example, the openapi-generator-maven-plugin plugin can automatically create your controller interfaces and DTOs directly from the specification file, including validation rules.
Evolving Your API Without Breaking It #
APIs change over time. But how do you add features without breaking existing clients?
Thiago demonstrated this by evolving an API. First, he added a new contacts field to the UserResponse DTO and a new POST /users endpoint. Was this a breaking change?
To find out, he used a handy tool: the openapi-diff-maven plugin. This plugin compares two versions of an OpenAPI specification and tells you if the changes are backward-compatible.
Conclusion: Adding a new, optional field or a new endpoint is not a breaking change. The API is still backward-compatible.
Next, he refactored the contacts to use inheritance, with a generic Contact type and specific PhoneContact and EmailContact types.
Conclusion: This is a breaking change. The old structure of the contact object is gone, and clients expecting it will fail. The openapi-diff plugin correctly identifies this as breaking backward compatibility.
When Breaking Changes Are Unavoidable: Versioning #
Sometimes, you have to make a breaking change. When that happens, you must introduce a new version of your API.
Thiago: “There are many strategies and as everything in IT, it depends. Each one has benefits and drawbacks, and choosing the best one will depend on your case.”
Here are the common versioning strategies:
- URL Path:
/v1/resource(Very common and clear) - Query Parameter:
resource?version=1 - Custom Header:
X-API-VERSION: 1(Keeps URLs clean) - Content Negotiation (Accept Header):
Accept: application/vnd.example.v1+json
What Constitutes a Breaking Change? #
To avoid versioning unless necessary, you need to know what a breaking change is. Here are some common examples:
- Changing an endpoint’s path (e.g., from
/users/{id}to/users/{id}/prefs). - Renaming a field in the JSON response or request.
- Adding a new, required field to the request body.
- Changing a field’s data type (e.g., from a
stringto anumber). - Removing a field from the response.
- Changing the response code for a given outcome (e.g., changing from
200to204).
Final Takeaways #
Building great REST APIs is a skill that takes practice, but following these guidelines will put you on the right path.
- Be Principled: Use the correct HTTP methods, status codes, and clear, noun-based naming conventions.
- Be Protective: Use DTOs to create a boundary between your API and your internal domain model. Never expose database entities directly.
- Be Proactive: Use an API-first approach with OpenAPI to design, document, and validate your APIs. Use tools like
openapi-diffto evolve your API safely. - Be Prepared: Understand what a breaking change is. If you must make one, use a clear versioning strategy to support your clients.
As Thiago wisely put it, it’s better to do the right thing from the beginning than to fix it later.