Resources vs. RPC semantic

Semantical correctness eases the consumption of services.

David Ibl
9 min readSep 12, 2018
rest

Semantical correctness is an important enabler when we talk about the ease of integration. A practical guide to the definition of REST resources is needed to get usable services.

This is the second part of a series about services following the REST pradigm.

  1. Documentation
  2. Resources vs. RPC semantic (this artice)
  3. Error handling (coming soon)
  4. Reducing server-side boilerplate in Java (coming soon)
  5. What is a robust service (coming soon)
  6. Reducing client-side boilerplate in Java (coming soon)
  7. Unit-testing APIs (coming soon)

The fundamental concept in any RESTful API is the resource. A resource is an object with a type, associated data, relationships to other resources, and a set of methods that operate on it. … Collections are themselves resources as well.

Well, as we think of a resource as a service providing instances of a defined type, we should be able to name the resource after this type.

Finally, this means, that the path of the resource is named after the type the resource provides.

Every resource has well-defined parameters identifying a specific operation on the server.

The important parts of this URL are:

  • Resource-Name
    The resource is normally named after the resource type. The name of the resource is an important part of the URL and follows the application base URL. To separate REST API endpoints from any other server operation, we defined that every concrete resource name follows the path ‘/api/’.
    Every path and resource name should be lowercase and camel-casing will be replaced by a hyphen. (type: AssuranceContract -> path: assurance-contract)
  • ID of an instance
    Formally the ID is part of the path. Next, to the resource name, it identifies one concrete instance of the type provided by a resource.
  • Further Path Segments
    Path segments following the instance identifier should provide access to properties of the given object. They, locate a partial tree within the object identified through the resource name. The name of the path should be the property name. It is always in lower case with camel-casing replaced by hyphens.
  • Query-String
    The query string is used to filter or configure the current operation.
  • HTTP-Verb
    The HTTP-Verb (GET, POST, PUT…) identifies the type of the operation.
  • Request/Response body
    The response/request body is needed to transport the payload of the given request.
  • Headers (optional)
    Meta information should be defined by request headers. E.g. process-ids or request-ids should be placed here.

Since we all need to implement applications not resources, in some points we defined custom behaviour to enable easy integration besides the common paradigms of RESTful APIs.

Let’s dive into some examples. To ease reading we group them by their HTTP-Verb.

GET

A resource with the GET-HTTP-Verb is used to query data. And finally, that is a law. A resource called with the GET-Verb never changes any data or writes anything else but logs or other meta information.

A GET request must never change or create any data.

It is very important to respect that. A GET resource is defined as idempotent call never changing any data. And with this believing, any integrator will use it. So integrators rely on that fact.

Examples:

GET /api/type-name

  • Provides a list containing every instance of the given type.
  • Will never answer with a 404 resource not found state, but with an empty list instead.
  • Can answer with any other error state since errors may happen.
  • Type-names are always singular, although they provide a list without an identifier. But if the single instance you retrieve by providing an identifier is a list it should be in plural.

GET /api/type-name?property-name=1234

  • Provides a list of object instances where an object property “propertyName” is “1234”.
  • Will never answer with a 404 resource not found state, but with an empty list instead.
  • Can answer with any other error state since errors may happen.
  • A concrete resource may or may not provide filters.
  • There may be multiple parameters which can be combined in any way.
  • If a given combination of resource parameters is not supported, after all, the server responds with an error state, not with an empty list!
  • The query-string can provide filtering mechanics besides object properties. For example, paging can be realized with query parameters. Finally, query parameters are always used to filter the response in any way.

GET /api/type-name?q=eyJwcm9wZXJ0eU5hbWUiOiAxMjM0fQ==

  • If the request model has to be expressed as a complex object, it should be passed as a base64 encoded JSON object. The client has to serialize this object and the server has to deserialize this object.

GET /api/type-name/{id}

  • Provides exactly one instance of the resource type, identified by the given id.
  • Answers with a 404 resource not found state if the requested instance is not found.
  • Can answer with any other error state since errors may happen.

GET /api/type-name/{id}/property-name

  • Provides access to a property of the target object.
  • The instance of the target object is identified by the given ID and the response will contain the value of the property identified by the given property name.
  • Answers with a 404 resource not found state when the instance identified by the id is not found.
  • Can answer with any other error state since errors may happen.
  • Answers with an empty response body if the value of the property is null. In this case, the server must never answer with a 404 resource not found state! The answer may be a 204 no content state. For historical reasons within our team, we also allowed the 200 ok state if the body is empty. This behaviour should be avoided after all.
  • If the given property itself is a list, the response may be empty after all. if the list itself is available but empty, the response should be an empty list.

POST

A POST request is used to create a new instance of the given type. And it is only used to create one.

POST /api/type-name — {request body with data}

  • Creates a new instance of the given type.
  • The server will answer with an error state if the id is already defined by the request body in any way. The id cannot be defined by the client when using a POST request.
  • Multiple requests with the same body will end up with multiple created instances. So this request is not idempotent.
  • Answers with the id of the created instance.
  • After a POST request, the client is able to retrieve the created instance with a GET request by using the id from the response.
  • In our team, we defined that a POST request may answer with the whole new instance for convenience reasons.

Special case: Complex query or POST for object

POST /api/type-name/search — { complex request model }

  • Returns a list of the given type.
  • Answers with an empty list if no instance matches the given query parameters, never with a 404 resource not found state.
  • Can answer with any other error state since errors may happen.
  • The resource path should contain a part like “search” to signalize the user that this resource behaves like a GET request.
  • Is only used in scenarios where the request model enforces this behaviour.
  • The server should answer with an error state if the client provides combinations of filter parameters not supported.
  • The prefered way to implement a complex query is the implementation of a GET request with an encoded query parameter (see above).
  • The POST for object request should always be documented and must never manipulate any data. Finally, this request type must be idempotent. Like a GET request.

PUT

A PUT request may create or manipulate an instance of the given type. The concrete id of the instance always has to be part of the request in any way, no matter if the instance is created or manipulated.

To create an instance with a PUT request, the client has to create the unique identifier.

PUT /api/type-name/{id} — {request body with data}

  • Always returns the object instance after its mutation.
  • Must never answer with a 404 resource not found error state.
  • Can answer with any other error state since errors may happen.
  • A PUT request must be idempotent by definition. The same request done twice should generate the same result! That’s an important behaviour, especially when we think about transaction control in the client process. Because of that, a PUT request is always the preferred way to create a resource.
  • A PUT request changes the whole given object. If the server behaves in any other way (ignoring null, only updating some supported fields) this behaviour must be documented.
  • After a PUT request, the client should be able to retrieve the exact same object through the corresponding GET request.

Special case: PUT a sub-resource

PUT /api/type-name/{id}/property-name — {request body with data}

  • We defined that this type of PUT request always ends up with a 204 no content state or an error.
  • The server might answer with a 404 resource not found state if the instance of the type with the given id cannot be found.
  • Will always change the value of the given property. If the server behaves different (e.g. ignoring null), this divergent behaviour has to be documented.
  • After a PUT request, the client should be able to retrieve the refreshed object state through a corresponding GET request with the same id.

DELETE

The usage of the DELETE HTTP-Verb is mostly self-explaining. Any request with a DELETE verb should delete the given instance.

DELETE /api/type-name/{id}

  • Will answer with the object deleted when successful. That’s maybe not the official convention but in many cases a convenient behaviour.
  • Must answer with a 404 resource not found state if the instance with the given id is not found. We defined this behaviour for easier client-side process control. Mostly it does not matter if the resource is not found or deleted. The result is the same.
    But there are cases where these two states have to be divided!
  • Can answer with any other error state since errors may happen.
  • Last but not least, the method should delete the instance with the given ID. We defined that behaviour in this way: After a DELETE request with a specific id, the corresponding GET endpoint has to answer with a 404 resource not found state, to a request with the same id.
    In many systems, entities are never really deleted, but from the perspective of any client, they should behave like they were deleted.

Special Cases

When converting SOAP RPC calls to REST resources, there is nearly always a point where things become complicated.

Since functionality was expressed in an operational manner when using SOAP services, we need to adjust them semantically to match semantical requirements of resource-oriented services.

In a real microservice-landscape maybe every operation can be represented as a simple PUT or POST request. But even then anybody anywhen needs to trigger the business process to start the orchestration of different services in the correct order with the correct data.

There are different approaches to implement this in a RESTful API.

We decided to use some kind of command pattern, to express the resource paths which don’t rely on concrete resources in form of an entity, but to business processes.

If we trigger complex operations related to entities we name our resources as events.

Interaction with business processes:

POST /api/processinstance/start — {request body with data}

  • As any POST request, this resource is not idempotent.
  • For every call, a process instance of the business process will be started.
  • The return value is always the unique id of the concrete process instance.

POST /api/processinstance/{id}/action/forward — {request body with data}

  • Used to POST a command to the process.
  • In this case, the forward-command is sent to the process.
  • Will answer with 204 no content state when successful.
  • Answers with 404 not found state if no corresponding process instance exists.
  • May answer with any other error as well.

Interaction with a complex entity related write operation:

POST /api/event-group/event-name?id={id} — {request body with data}

  • The definition of write operations is inspired by some kind of event-sourcing.
  • In many cases, the event itself never gets an own unique id, so the POST request does not fulfil the full formal requirements on a POST request.
  • How the service handles an event internally is transparent to the client.

--

--

David Ibl

Chief Platform Officer @lv1871 the fanciest assurance company