REST (REpresentational State Transfer) is an inter-operable protocol for services that is more lightweight than SOAP.
However, it is no real standard and can cause confusion. Therefore we define best practices here to guide you.
ATTENTION:
REST and RESTful often implies very strict and specific rules and conventions. However different people will often have different opinions of such rules. We learned that this leads to "religious discussions" (starting from PUT
vs. POST
and IDs in path vs. payload up to Hypermedia and HATEOAS). These "religious discussions" waste a lot of time and money without adding real value in case of common business applications (if you publish your API on the internet to billions of users this is a different story). Therefore we give best practices that lead to simple, easy and pragmatic "HTTP APIs" (to avoid the term "REST services" and end "religious discussions"). Please also note that we do not want to assault anybody nor force anyone to follow our guidelines. Please read the following best practices carefully and be aware that they might slightly differ from what your first hit on the web will say about REST (see e.g. RESTful cookbook).
URLs are not case sensitive. Hence, we follow the best practice to use only lower-case-letters-with-hyphen-to-separate-words. For operations in REST we distinguish the following types of URLs:
-
A collection URL is build from the rest service URL by appending the name of a collection. This is typically the name of an entity. Such URI identifies the entire collection of all elements of this type. Example:
https://mydomain.com/myapp/services/rest/mycomponent/v1/myentity
-
An element URL is build from a collection URL by appending an element ID. It identifies a single element (entity) within the collection. Example:
https://mydomain.com/myapp/services/rest/mycomponent/v1/myentity/42
-
A search URL is build from a collection URL by appending the segment
search
. The search criteria is send asPOST
. Example:https://mydomain.com/myapp/services/rest/mycomponent/v1/myentity/search
This fits perfect for CRUD operations. For business operations (processing, calculation, etc.) we simply create a collection URL with the name of the business operation instead of the entity name (use a clear naming convention to avoid collisions). Then we can POST
the input for the business operation and get the result back.
If you want to provide an entity with a different structure do not append further details to an element URL but create a separate collection URL as base.
So use https://mydomain.com/myapp/services/rest/mycomponent/v1/myentity-with-details/42
instead of https://mydomain.com/myapp/services/rest/mycomponent/v1/myentity/42/with-details
.
For offering a CTO simply append -cto
to the collection URL (e.g. …/myentity-cto/
).
While REST was designed as a pragmatical approach it sometimes leads to "religious discussions" e.g. about using PUT
vs. POST
(see ATTENTION notice above).
As the devonfw has a string focus on usual business applications it proposes a more "pragmatic" approach to REST services.
On the next table we compare the main differences between the "canonical" REST approach (or RESTful) and the devonfw proposal.
HTTP Method | RESTful Meaning | devonfw |
---|---|---|
|
Read single element. Search on an entity (with parametrized url) |
Read a single element. |
|
Replace entity data. Replace entire collection (typically not supported) |
Not used |
|
Create a new element in the collection |
Create or update an element in the collection. Search on an entity (parametrized post body) Bulk deletion. |
|
Delete an entity. Delete an entire collection (typically not supported) |
Delete an entity. Delete an entire collection (typically not supported) |
Please consider these guidelines and rationales:
-
We use
POST
on the collection URL to save an entity (create
if no ID provided in payload otherwiseupdate
). This avoids pointless discussions in distinctions betweenPUT
andPOST
and what to do if acreate
contains an ID in the payload or if anupdate
is missing the ID property or contains a different ID in payload than in URL. -
Hence, we do NOT use
PUT
but always usePOST
for write operations. As we always have a technical ID for each entity, we can simply distinguish create and update by the presence of the ID property. -
Please also note that for (large) bulk deletions you may be forced to used
POST
instead ofDELETE
as according to the HTTP standardDELETE
must not have payload and URLs are limited in length.
Further we define how to use the HTTP status codes for REST services properly. In general the 4xx codes correspond to an error on the client side and the 5xx codes to an error on the server side.
HTTP Code | Meaning | Response | Comment |
---|---|---|---|
200 |
OK |
requested result |
Result of successful GET |
204 |
No Content |
none |
Result of successful POST, DELETE, or PUT (void return) |
400 |
Bad Request |
error details |
The HTTP request is invalid (parse error, validation failed) |
401 |
Unauthorized |
none (security) |
Authentication failed |
403 |
Forbidden |
none (security) |
Authorization failed |
404 |
Not found |
none |
Either the service URL is wrong or the requested resource does not exist |
500 |
Server Error |
error code, UUID |
Internal server error occurred (used for all technical exceptions) |
devonfw has support for the following metadata in REST service invocations:
Name | Description | Further information |
---|---|---|
X-Correlation-Id |
HTTP header for a correlation ID that is a unique identifier to associate different requests belonging to the same session / action |
|
Validation errors |
Standardized format for a service to communicate validation errors to the client |
Server-side validation is documented in the Validation guide. The protocol to communicate these validation errors is described in REST exception handling. |
Pagination |
Standardized format for a service to offer paginated access to a list of entities |
Server-side support for pagination is documented in the Repository Guide. |
For implementing REST services we use the JAX-RS standard. As an implementation we recommend CXF. For JSON bindings we use Jackson while XML binding works out-of-the-box with JAXB.
To implement a service you write an interface with JAX-RS annotations for the API and a regular implementation class annotated with @Named
to make it a spring-bean. Here is a simple example:
com.devonfw.application.mtsj.dishmanagement.service.impl.rest
@Path("/imagemanagement/v1")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface ImagemanagementRestService {
@GET
@Path("/image/{id}/")
public ImageEto getImage(@PathParam("id") long id);
}
@Named("ImagemanagementRestService")
public class ImagemanagementRestServiceImpl implements ImagemanagementRestService {
@Inject
private Imagemanagement imagemanagement;
@Override
public ImageEto getImage(long id) {
return this.imagemanagement.findImage(id);
}
}
Here we can see a REST service for the business component imagemanagement
. The method getImage
can be accessed via HTTP GET (see @GET
) under the URL path imagemanagement/image/{id}
(see @Path
annotations) where {id}
is the ID of the requested table and will be extracted from the URL and provided as parameter id
to the method getImage
. It will return its result (ImageEto
) as JSON (see @Produces
- should already be defined as defaults in RestService
marker interface). As you can see it delegates to the logic component imagemanagement
that contains the actual business logic while the service itself only exposes this logic via HTTP. The REST service implementation is a regular CDI bean that can use dependency injection.
The separation of the API as a Java interface allows to use it for service client calls.
Note
|
With JAX-RS it is important to make sure that each service method is annotated with the proper HTTP method (@GET ,@POST ,etc.) to avoid unnecessary debugging. So you should take care not to forget to specify one of these annotations.
|
For exceptions a service needs to have an exception façade that catches all exceptions and handles them by writing proper log messages and mapping them to a HTTP response with an according HTTP status code. Therefore the devonfw provides a generic solution via RestServiceExceptionFacade
. You need to follow the exception guide so that it works out of the box because the façade needs to be able to distinguish between business and technical exceptions.
Now your service may throw exceptions but the façade with automatically handle them for you.
The devonfw proposes, for simplicity, a deviation from the common REST pattern:
-
Using
POST
for updates (instead ofPUT
) -
Using the payload for addressing resources on POST (instead of identifier on the
URL
) -
Using parametrized
POST
for searches
This use of REST will lead to simpler code both on client and on server. We discuss this use on the next points.
The following table specifies how to use the HTTP methods (verbs) for collection and element URIs properly (see wikipedia).
-
HTTP Method:
GET
-
URL example:
/products/123
For loading of a single resource, embed the identifier
of the resource in the URL (for example /products/123
).
The response contains the resource in JSON format, using a JSON object at the top-level, for example:
{
"name": "Steak",
"color": "brown"
}
-
HTTP Method:
GET
-
URL example:
/products
For loading of a collection of resources, make sure that the size of the collection can never exceed a reasonable maximum size. For parameterized loading (searching, pagination), see below.
The response contains the collection in JSON format, using a JSON object at the top-level, and the actual collection underneath a result
key, for example:
{
"result": [
{
"name": "Steak",
"color": "brown"
},
{
"name": "Broccoli",
"color": "green"
}
]
}
-
HTTP Method:
POST
-
URL example:
/products
The resource will be passed via JSON in the request body. If updating an existing resource, include the resource’s identifier
in the JSON and not in the URL, in order to avoid ambiguity.
If saving was successful, an empty HTTP 204 response is generated.
If saving was unsuccessful, refer below for the format to return errors to the client.
-
HTTP Method:
POST
-
URL example:
/products/search
In order to differentiate from an unparameterized load, a special subpath (for example search
) is introduced. The parameters are passed via JSON in the request body. An example of a simple, paginated search would be:
{
"status": "OPEN",
"pagination": {
"page": 2,
"size": 25
}
}
The response contains the requested page of the collection in JSON format, using a JSON object at the top-level, the actual page underneath a result
key, and additional pagination information underneath a pagination
key, for example:
{
"pagination": {
"page": 2,
"size": 25,
"total": null
},
"result": [
{
"name": "Steak",
"color": "brown"
},
{
"name": "Broccoli",
"color": "green"
}
]
}
Compare the code needed on server side to accept this request: com.devonfw.application.mtsj.dishmanagement.service.api.rest
@Path("/category/search")
@POST
public PaginatedListTo<CategoryEto> findCategorysByPost(CategorySearchCriteriaTo searchCriteriaTo) {
return this.dishmanagement.findCategoryEtos(searchCriteriaTo);
}
With the equivalent code required if doing it the RESTful way by issuing a GET
request:
@Path("/category/search")
@POST @Path("/order")
@GET
public PaginatedListTo<CategoryEto> findCategorysByPost( @Context UriInfo info) {
RequestParameters parameters = RequestParameters.fromQuery(info);
CategorySearchCriteriaTo criteria = new CategorySearchCriteriaTo();
criteria.setName(parameters.get("name", Long.class, false));
criteria.setDescription(parameters.get("description", OrderState.class, false));
criteria.setShowOrder(parameters.get("showOrder", OrderState.class, false));
return this.dishmanagement.findCategoryEtos(criteria);
}
The client can choose to request a count of the total size of the collection, for example to calculate the total number of available pages. It does so, by specifying the pagination.total
property with a value of true
.
The service is free to honour this request. If it chooses to do so, it returns the total count as the pagination.total
property in the response.
-
HTTP Method:
DELETE
-
URL example:
/products/123
For deletion of a single resource, embed the identifier
of the resource in the URL (for example /products/123
).
The general format for returning an error to the client is as follows:
{
"message": "A human-readable message describing the error",
"code": "A code identifying the concrete error",
"uuid": "An identifier (generally the correlation id) to help identify corresponding requests in logs"
}
If the error is caused by a failed validation of the entity, the above format is extended to also include the list of individual validation errors:
{
"message": "A human-readable message describing the error",
"code": "A code identifying the concrete error",
"uuid": "An identifier (generally the correlation id) to help identify corresponding requests in logs",
"errors": {
"property failing validation": [
"First error message on this property",
"Second error message on this property"
],
// ....
}
}
The payload of a REST service can be in any format as REST by itself does not specify this. The most established ones that the devonfw recommends are XML and JSON. Follow these links for further details and guidance how to use them properly. JAX-RS
and CXF
properly support these formats (MediaType.APPLICATION_JSON
and MediaType.APPLICATION_XML
can be specified for @Produces
or @Consumes
). Try to decide for a single format for all services if possible and NEVER mix different formats in a service.
For testing REST services in general consult the testing guide.
For manual testing REST services there are browser plugins:
-
Firefox: httprequester (or poster)
-
Chrome: postman (advanced-rest-client)
Your services are the major entry point to your application. Hence security considerations are important here.
A common security threat is CSRF for REST services. Therefore all REST operations that are performing modifications (PUT, POST, DELETE, etc. - all except GET) have to be secured against CSRF attacks. In devon4j we are using spring-security that already solves CSRF token generation and verification. The integration is part of the application template as well as the sample-application.
For testing in development environment the CSRF protection can be disabled using the JVM option -DCsrfDisabled=true
when starting the application.
OWASP suggests to prevent returning JSON arrays at the top-level, to prevent attacks (see https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines). However, no rationale is given at OWASP. We digged deep and found anatomy-of-a-subtle-json-vulnerability. To sum it up the attack is many years old and does not work in any recent or relevant browser. Hence it is fine to use arrays as top-level result in a JSON REST service (means you can return List<Foo>
in a Java JAX-RS service).