Skip to content

Latest commit

 

History

History
298 lines (239 loc) · 16.9 KB

contract.md

File metadata and controls

298 lines (239 loc) · 16.9 KB

Contract

This chapter covers how the different API styles let API developers define their operations: which is the recommended approach, what alternatives there are, which Interface Definition Languages can be used, and so on. This chapter does not cover how to define schemas to shape our models (see schema definition for this), nor how to express each operation (see methods).

REST

For a Web API to be called RESTful, it is mandatory that it is hypermedia-based (HATEOAS). This means all their resources can be traversed with no prior knowledge of their URI, but as a succession of links. This mechanism, that inspired Roy Fielding when creating REST, has been proven successful in the World Wide Web: people browse the Web as a series of links. However, most REST APIs are not HATEOAS compliant. So, instead of taking advantage of hypermedia, very often APIs rely on contracts and uri templates on how identifiers are built; links are not opaque anymore, but predictable. These APIs, which are like resource-based RPC, are often called REST-Like Web Services. Many people think that, despite being a good idea, true RESTful market is far from being mature.

Let's see each one of these REST styles.

True REST

According to Roy Fielding:

If the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.

Characteristics of a HATEOAS API:

  • ✔️ Evolvability. Client and server don't rely on a bespoke contract, but in open standards.
  • ✔️ Discoverability. A client can automatically discover new functionalities provided by the server.
  • ✘ Lack of adoption and tooling.
  • ✘ Cost. Developing a RESTful Service is expensive.

What this means is that each resource is somehow connected to other resources, thus creating a graph. If a client application uses that graph as its engine, then that means that the graph is a state machine, where each resource/node is a state, and each link/connection is a state transition. Let's think of a microwave oven. When we first get the microwave, its representation might contain an action to turn it on:

// GET /microwaves/12

{
    "state": "off",
    "actions": [
        {
            "rel": "on",
            "href": "/microwaves/12",
            "method": "PUT",
            "Expects": { "state": "on"}
        }
    ]
}

The actions property contains a list of possible transitions in a certain hypermedia specification, so that the client only needs to follow links. In this case, to turn the microwave on we just need to following the on link. This is HATEOAS in action. A client is only required to understand the semantics of each available transition.

Code need not rely on any URI convention. According to Roy Fielding words,

A REST API should be entered with no prior knowledge beyond the initial URI (bookmark) and set of standardized media types that are appropriate for the intended audience (i.e., expected to be understood by any client that might use the API).

So, technically, all these URI might be completely RESTful:

Links specifications

There is no unique medium to express them:

Typically, the type of relation in a link is specified in a rel field. IANA maintains a list of standard link relations.

About rel

Some people (See RESTful Web Services Cookbook) suggest we express the action type in the rel following these rules:

  • Use the existing names, like self, alternate, related, previous, next, first and last.
  • If there is no existing name, create a new one. Express that relation as a URI. Also, provide an HTML documentation for that relation at that URI.

Real World Examples

Criticisms of HATEOAS often argue that there are no real-world examples of it, which is unfair. One of the most widely-used REST APIs in the world makes use of it: PayPal. Here is an example extract from a response:

{
  "links": [{
    "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832",
    "rel": "self",
    "method": "GET"
  }, {
    "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832/refund",
    "rel": "refund",
    "method": "POST"
  }, {
    "href": "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
    "rel": "parent_payment",
    "method": "GET"
  }]
}

Note on Versioning

When it comes to versioning, according to Roy Fielding keynote on Evolve'13, the best practice for versioning a REST API is not to version it. REST is already defined as a state machine (HATEOAS) where each state can be dynamic and each transition can be redirected (linked). So instead of agreeing on an interface, to change the state, client software should only need to follow the appropriate links (as we humans do when we use a web). But still, an API might need updates which break backwards compatibility (like fixing a typo in a schema).

Tooling

As with the Web, a HATEOAS based API needs somewhere to start from. For this, we typically use the root page. Then, from there we can traverse the whole system.

This way of interaction is convenient for humans. With unattended applications we will need prior knowledge about what links there are, so that we can programmatically get to them. This knowledge can be provided by the API itself (REST APIs are highly discoverable). We can use tools like HAL Browser to navigate the API.

With regard to unattended applications, there are libraries to simplify the traversal of standard hypermedia links in REST APIs, like Traverson (which comes with builtin support for JSON API links).

REST-Like

Although Hypermedia is a very powerful tool, most developers choose a different approach when the consumer of the API is another application that wants to provide its own experience to the final user. Here, URIs are no longer opaque, but constructed by the client. This is often known as REST-Like, so-called REST or simply OpenAPI, and basically it is a resource-oriented RPC Web API, just like gRPC, but unlike gRPC, in REST-Like the underlying transport protocol, HTTP, is exposed to the client.

The fact that this is not pure REST is not necessarily bad. Actually, this type of API design is so popular that it is the one followed by well-known frameworks like OpenAPI, API Blueprint or RAML. And, in spite of the evolvability benefits from an hypermedia-based API, some people is skeptical about whether we are ready for them.

URI Templates and URI design

Even though according to the REST constraints neither the client nor the documentation should rely on a specific URI convention, that does not mean that we cannot follow a convention to (1) make the URIs human-readable, (2) to save design time or (3) to distribute the processing based on our URIs path. It is completely right to use, for example, URI Templates (RFC 6570).

Many of the rules on how to design URIs are opinionated. Still, some have major approval in the community, like the ones presented in REST API Design Rulebook, by Mark Masse:

  • Forward slash (/): won't be used as the last character of a URI. It is used to specify a hierarchical relationship. This allows for mapping compositions of elements.
  • Use hyphens (-), and not underscore (_), to improve readability.
  • Use lowercase.
  • Do not include file extensions. Use the HTTP Accept header instead.

There is almost a consensus about whether to use plural or singular names:

  • Document - use a singular noun: https://example.com/universities/urjc
  • Collection and store - use a plural noun: https://example.com/universities
  • Controller - use a verb: https://example.com/albums/341/play

OpenAPI

OpenAPI is a specification to describe what an HTTP API can do. This is done writing an OpenAPI spec using either Yaml or Json.

  • It's standard. So it is well suited for humans, to understand an API, but also for machines: they can render it as an interactive documentation (see Swagger UI or ReDoc) or autogenerate code for us (see Swagger CodeGen).
  • Describes the paths (endpoints), with the supported operations, parameters and resources.
  • Can specify data models using JSON Schema.
  • Since OpenAPI 3.0, links are supported, which allows for using the value returned by an operation as the input of another operation. Although this is not technically HATEOAS, it's an alternative to hypermedia used in REST.

Example: This example, taken from the Swagger website, outlines how to add the metainformation of a server, and its path /users/{userId}, reachable through GET:

openapi: 3.0.0
info:
  title: Sample API
  description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
  version: 0.1.9
servers:
  - url: http://api.example.com/v1
    description: Optional server description, e.g. Main (production) server
  - url: http://staging-api.example.com
    description: Optional server description, e.g. Internal staging server for testing
paths:
  /users/{userId}:
    get:
      summary: Gets a user by ID
      operationId: getUser
      parameters:
        - in: path
          name: userId
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: A User object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
          readOnly: true
        name:
          type: string

The schema, expressed in JSON Schema, describes how a User object is organized. Then, that schema is referenced from the 200 response of the getUser operation.

GraphQL

GraphQL contrasts with REST on how easy it is to define the available operations. Three Operation Types are defined (see methods for more info on what each operation do) in the schema section:

  • query
  • mutation
  • subscription

The GraphQL Schema Language let us specify each of these operations in their own section:

{
    type Query {
        user(param: Int): User
    }

    type Mutation {
        updateUser(param: UpdateUserInput!): User
    }

    type Subscription {
        newUser: User
    }

    schema {
        query: Query
        mutation: Mutation
        subscription: Subscription
    }
}

Internally, server implementations will map each field to a resolver. These resolvers are functions, provided by the API developer, that returns the value for a field.

Note: we have defined three types, each mapping to its operation type. We can have used whatever name, but its a convention to name Query, Mutation and Subscription each of these types. Also note that our Query type is just a regular Object Type, with a number of typed fields that can optionally define arguments, as any other Object Type.

Arguments and return values

Each operation acts as an RPC method that accept both arguments and a return value. Arguments, which are optional, can be used to for example let a List operation filter the results, or a Get operation select a specific resource.

In addition to optional input values, every GraphQL operation will return an element. If this element is of any compound type, caller will need to provide the fields it is interested in:

query {
    myFirstQuery(param: 12) {
        field1
        field2
    }
}

gRPC

gRPC uses gRPC uses Protocol Buffers as the Interface Definition Language for describing its contract and to define its schema. To define a service interface in Protocol Buffers, the Service type will be used:

service myService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

Here, we define a named service, myService, that exposes a single entry point, Searcher, which accepts an argument of type SearchRequest and returns a message of type SearchResponse. Note that even though this service definition in Protocol Buffers is the de-facto standard for gRPC, it can also be used with other API styles, for example with REST.

gRPC with REST and GraphQL

It is also possible to implement a REST Web Service using gRPC. For this, we will annotate the .proto definition with google.api.http to map an RPC with an HTTP request:

service myService {
  rpc CreateArticle(CreateArticleRequest)
    returns (Article)
  {
    option (google.api.http) = {
      post: "/articles"
      body: "article"
    };
  }
}

message CreateArticleRequest {
  Article article = 1;
}

Then, using the grpc-gateway (a plugin for protoc), a RESTful Web Service will be automatically generated.

There is also the rejoiner project, which aims to generate a GraphQL schema out of gRPC services.

Resources