Every API style sets some boundaries in how we design our interface. For example, in GraphQL we need to express every operation in terms of queries and mutations, whereas in REST they will be mapped to any verb of the underlying transport protocol. Some operations are straightforward, for example a read, while others might be tricky. Let's see how to go about them.
We call a pure function an operation that has no side effects. Many API calls can be thought as pure functions. For example, a method which transforms from Fahrenheit to Celsius, or an action which calculates the distance between two cities.
We can use GET
to express a verb of a safe and idempotent operation. For example, let's consider the distance between cities example; in this case, the resource would be the input of the URI, and the representation would be the distance returned, as in:
/distance/Madrid/Seville
Note this might not be pure REST, as this URI wouldn't be opaque.
In HTTP, a GET
request is expected to be safe (i.e. with no side-effects). One might think GraphQL queries must not have side-effects either. Although it is not mandatory, queries usually are safe. To express an operation that does not have side-effects, we can use a query. An example of this is calculate the distance between two cities:
type Query {
distance(from: String!, to: String!): Distance!
}
A regular rpc
operation can be used to express a pure function:
service MyService {
rpc GetDistance(DistanceRequest)
returns (DistanceResponse);
}
message DistanceRequest {
string from = 1;
string to = 2;
}
message DistanceResponse {
int32 distance = 1;
}
Resources have both state and state transitions. For example, an order might be pending, paid, shipped or delivered. We can think of a resource as a state machine with allowed transitions.
On a truly RESTFul Web Service that follows the HATEOAS constraint, a custom action can be expressed just as a possible state transition to a resource.
For simple cases, we can also map a state to a field: a field called status
for a music player, which accepts a number of possible options, or a field called activated
of type boolean.
In addition, a new resource can be created as well. These resources will map actions into them. For example, we can use this technique to send a recovery password email to a user. The new embedded resource will belong to user
and might contain the sent_date
and the hash_code
used in the recovery URL.
Looking at real world REST APIs, we find that GitHub defined an embedded resource in gists (a form of shareable snippets of code) to star or unstar them, as in PUT|DELETE /gists/:gist_id/start
.
Paypal as well allows to authorize a payment creating a resource of type authorize
into an order: PUT /v2/checkout/orders/5O190127TN364715T/authorize
.
A mutation will be used to express a change state. This can be done either in a resource-oriented or in a action-oriented approach. If our API is resource-oriented, we can define a custom field which contains the state, as in a field called status
. To update it, a generic update mutation can be used, or a specific mutation:
type Mutation {
updateOrder(order: Order!): Order!
updateOrderStatus(orderId: ID!, status: Status!): Order!
}
If the API follows an action-oriented style, then it might expose specific methods. For example, to turn a microwave on:
type Mutation {
turnMicrowaveOn(microwaveId: ID!): Microwave!
}
There is no such think as HATEOAS for GraphQL. This means client applications will need more knowledge, but also they will do a better use of the network traffic.
To express a state transition, a status
field of type enum can be used, as in:
service Shop {
rpc updateOrder(UpdateOrderRequest)
returns (Order);
}
message UpdateOrderRequest {
Order order = 1;
FieldMask update_mask = 2;
}
message Order {
string id = 1;
OrderStatus status = 2;
}
enum OrderStatus {
PENDING = 1;
PAID = 2;
SHIPPED = 3;
DELIVERED = 4;
}
Then the FieldMask
will may be used to update only the status
field.
Additionally, for common status, a custom rpc might be created. For example, a Cancel
method can be created to cancel an order:
service Shop {
rpc cancelOrder(CancelOrderRequest)
returns (Order);
}
message CancelOrderRequest {
string name = 1;
}
Even though almost any operation can be expressed in terms of just state transitions, sometimes this approach does not naturally fit our action. There are several ways to workaround this for each API style.
Another option available to REST APIs is creating a resource of type controller. This can be used for a number of cases:
- To merge two resources.
- To create a copy of a resource.
- To move a resource.
- To run bulk operations.
Chapter 11 of RESTful Web Services Cookbook gives lot of details on how these controllers can be articulated. Google Cloud API Design guide also provides good examples on when and how to create custom methods.
As its name suggest, mutations will be used to express any other operation that changes the state of a resource. But, unlike other styles, GraphQL does not impose any restriction on how to address this operation: it can be resource-oriented or it can be action-oriented. For example, we can define a mutation to merge two users:
type Mutation {
mergeUsers(from:ID!,to:ID!): User!
}
In addition, GraphQL allows for batch operations, either queries
or mutations
, as in:
mutation UpdateTwoProducts($product1:ProductInput!, ($product2:ProductInput!) {
update1: updateProduct(product: $product1) {
id
price
}
update2: updateProduct(product: $product2) {
id
price
}
}
Any other operation can be express without restrictions in an rpc
message type. Google Cloud API Design Guide suggest that developers follow a standard naming policy for these regular operations, as in:
Cancel
- see aboveBatchGet
Move
Search
- see method List
The demo project contains a naive example of how to implement a custom operation. Specifically, using a pure function to calculate the distance between to cities.
To get the distance between Madrid an Barcelona in REST, we can just:
curl -v http://localhost:4000/distances/madrid/barcelona
It will return a distance expressed in text/plain
.
To get the distance between Madrid
and Barcelona
, run this query:
{
distance(from:"Madrid",to:"Barcelona") {
from
to
km
}
}
The gRPC project contains an rpc
to calculate a distance:
service MyService {
rpc distance(DistanceRequest)
returns (DistanceReply);
}
message DistanceRequest {
string from = 1;
string to = 2;
}
message DistanceReply {
string from = 1;
string to = 2;
int32 km = 3;
}
Run the client application, npm run grpcc
, and ask for a distance:
client.getDistance({from:"Madrid", to:"Barcelona"}, pr);