Skip to content

Latest commit

 

History

History
332 lines (251 loc) · 18.3 KB

guide-rest.asciidoc

File metadata and controls

332 lines (251 loc) · 18.3 KB

REST

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

URLs are not case sensitive. Hence, we follow the best practice to use only lower-case-letters-with-hypen-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 as POST. 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/).

HTTP Methods

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.

Table 1. Usage of HTTP methods
HTTP Method RESTful Meaning devonfw

GET

Read single element.

Search on an entity (with parametrized url)

Read a single element.

PUT

Replace entity data.

Replace entire collection (typically not supported)

Not used

POST

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

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 otherwise update). This avoids pointless discussions in distinctions between PUT and POST and what to do if a create contains an ID in the payload or if an update is missing the ID property or contains a different ID in payload than in URL.

  • Hence, we do NOT use PUT but always use POST 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 of DELETE as according to the HTTP standard DELETE must not have payload and URLs are limited in length.

HTTP Status Codes

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.

Table 2. Usage of HTTP status codes
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)

Metadata

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

Logging guide

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 to the client is worked on at oasp/oasp4j#218

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.

JAX-RS

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.

JAX-RS Configuration

Starting from CXF 3.0.0 it is possible to enable the auto-discovery of JAX-RS roots.

When the jaxrs server is instantiated all the scanned root and provider beans (beans annotated with javax.ws.rs.Path and javax.ws.rs.ext.Provider) are configured.

REST Exception Handling

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.

Recommendations for REST requests and responses

The devonfw proposes, for simplicity, a deviation from the common REST pattern:

  • Using POST for updates (instead of PUT)

  • 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).

Unparameterized loading of a single resource

  • 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"
}

Unparameterized loading of a collection of resources

  • 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"
        }
    ]
}

Saving a resource

  • 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.

Parameterized loading of a resource

  • 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);

  }

Pagination details

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.

Deletion of a resource

  • 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).

Error results

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"
        ],
        // ....
    }
}

REST Media Types

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.

REST Testing

For testing REST services in general consult the testing guide.

For manual testing REST services there are browser plugins:

Security

Your services are the major entry point to your application. Hence security considerations are important here.

CSRF

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.

JSON top-level arrays

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).