Organize the API around resources
Focus on the business entities that the web API exposes. For example, in an e-commerce system, the primary entities might be customers and orders. Creating an order can be achieved by sending an HTTP POST request that contains the order information. The HTTP response indicates whether the order was placed successfully or not. When possible, resource URIs should be based on nouns (the resource) and not verbs (the operations on the resource).
https://adventure-works.com/orders // Good
https://adventure-works.com/create-order // Avoid
A resource doesn't have to be based on a single physical data item. For example, an order resource might be implemented internally as several tables in a relational database, but presented to the client as a single entity. Avoid creating APIs that simply mirror the internal structure of a database. The purpose of REST is to model entities and the operations that an application can perform on those entities. A client should not be exposed to the internal implementation.
Entities are often grouped together into collections (orders, customers). A collection is a separate resource from the item within the collection, and should have its own URI. For example, the following URI might represent the collection of orders:
https://adventure-works.com/orders
Sending an HTTP GET request to the collection URI retrieves a list of items in the collection. Each item in the collection also has its own unique URI. An HTTP GET request to the item's URI returns the details of that item.
Adopt a consistent naming convention in URIs. In general, it helps to use plural nouns for URIs that reference collections. It's a good practice to organize URIs for collections and items into a hierarchy. For example, /customers
is the path to the customers collection, and /customers/5
is the path to the customer with ID equal to 5. This approach helps to keep the web API intuitive. Also, many web API frameworks can route requests based on parameterized URI paths, so you could define a route for the path /customers/{id}
.
Also consider the relationships between different types of resources and how you might expose these associations. For example, the /customers/5/orders
might represent all of the orders for customer 5. You could also go in the other direction, and represent the association from an order back to a customer with a URI such as /orders/99/customer
. However, extending this model too far can become cumbersome to implement. A better solution is to provide navigable links to associated resources in the body of the HTTP response message. This mechanism is described in more detail in the section Use HATEOAS to enable navigation to related resources.
In more complex systems, it can be tempting to provide URIs that enable a client to navigate through several levels of relationships, such as /customers/1/orders/99/products
. However, this level of complexity can be difficult to maintain and is inflexible if the relationships between resources change in the future. Instead, try to keep URIs relatively simple. Once an application has a reference to a resource, it should be possible to use this reference to find items related to that resource. The preceding query can be replaced with the URI /customers/1/orders
to find all the orders for customer 1, and then /orders/99/products
to find the products in this order.
Tip
Avoid requiring resource URIs more complex than collection/item/collection.
Avoid chatty endpoints
Another factor is that all web requests impose a load on the web server. The more requests, the bigger the load. Therefore, try to avoid "chatty" web APIs that expose a large number of small resources. Such an API may require a client application to send multiple requests to find all of the data that it requires. Instead, you might want to denormalize the data and combine related information into bigger resources that can be retrieved with a single request. However, you need to balance this approach against the overhead of fetching data that the client doesn't need. Retrieving large objects can increase the latency of a request and incur additional bandwidth costs.
Avoid introducing dependencies between the web API and the underlying data sources. For example, if your data is stored in a relational database, the web API doesn't need to expose each table as a collection of resources. In fact, that's probably a poor design. Instead, think of the web API as an abstraction of the database. If necessary, introduce a mapping layer between the database and the web API. That way, client applications are isolated from changes to the underlying database scheme.
Finally, it might not be possible to map every operation implemented by a web API to a specific resource. You can handle such non-resource scenarios through HTTP requests that invoke a function and return the results as an HTTP response message. For example, a web API that implements simple calculator operations such as add and subtract could provide URIs that expose these operations as pseudo resources and use the query string to specify the parameters required. For example, a GET request to the URI /add?operand1=99&operand2=1 would return a response message with the body containing the value 100. However, only use these forms of URIs sparingly.
Create Model/Resource URIs
The key abstraction of information in REST is a resource. Any information that can be named can be a resource: a document or image, a temporal service, a collection of other resources, a non-virtual object (e.g. a person), and so on. REST uses a resource identifier to identify the particular resource involved in an interaction between components.
Now when object model is ready, it’s time to decide the resource URIs. At this step, while designing the resource URIs – focus on the relationship between resources and its sub-resources. These resource URIs are endpoints for RESTful services.
In our application, a device is a top-level resource. And configuration is sub-resource under device. Let’s write down the URIs.
/devices
/devices/{id}
/configurations
/configurations/{id}
/devices/{id}/configurations
/devices/{id}/configurations/{id}
Resource Methods
Other important thing associated with REST is resource methods to be used to perform the desired transition. A large number of people wrongly relate resource methods to HTTP GET/PUT/POST/DELETE methods.
If you decide HTTP POST
will be used for updating a resource – rather than most people recommend HTTP PUT
– it’s alright and application interface will be RESTful.
Ideally, everything that is needed to change the resource state shall be part of API response for that resource – including methods and in what state they will leave the representation.
Use plural nouns and not verbs
As an example, imagine you want a service for stock control of fruits.
Here is a simple chart of basic REST APIs you can make for your fruit application:
Resource | GET read | POST create | PUT update | DELETE |
---|---|---|---|---|
/fruits | Returns a list of fruits possibly all the fruits you have | Create a new fruit | Bulk update of fruits (you may never have to do this) | Delete all fruits (you should probably not implement this) |
/fruits/1004 | Returns a specific fruit | Method not allowed (405) | Updates a specific fruit | Deletes a specific fruit |
It’s not good practice to have API endpoints like /getFruits or /delete-fruits or /createfruits. Instead, take advantage of HTTP verbs for the definitions. This means I can easily build upon your APIs and have fewer resources to manage. This approach is less ambiguous and cleaner.
Warning
Try not to mix singular and plural nouns. Use plural nouns since you typically fetch resources by their IDs. There are situations where singular nouns work, but they should be use sparingly.
Don't return plain text
Although it is not imposed by the REST architectural style, most REST APIs use JSON as a data format.
However, it is not enough to return a body containing a JSON-formatted string. You need to specify the Content-Type header too! It must be set to the value application/json.
This is especially important for programmatic clients (for example, someone or a service interacting with your API via the requests library in Python) — some of them rely on this header to correctly decode the response.