Aller au contenu

Introduction to OpenAPI and Tools

Introduction

What is the need ?

The Web was born around the client/server model, where the Web client requests a resource and the Web server serves this resource. Exchanging information between a client and a server requires the definition of an Application Programming Interface or API.

Standardizing the way an API is specified is a need for many reasons. One important reason is that it allows the creation of tools for presenting, testing and generating codes. The OpenAPI initiative is one of the most popular initiatives for creating, evolving and promoting a vendor neutral RESTful API description format. Based on this description, that was originally based on the Swagger Specification, a huge number of tools have been developed by a large community of developers, for easing the documentation and development of a RESTful API (see OpenAPI tools for instance).

What you’ll build

You will first go through your first OpenAPI specification using a simple RESTful API example. Once the API and its documentation are understood, you will first install the development environment, together with the plugins helping to understand, further document, test and develop the RESTful API.

What you’ll learn

  • The basics of the JSON format.
  • How to read and create the specification of a RESTful API using the OpenAPI standard.
  • How to test an existing API.
  • Important rules for designing a RESTful API.

What you’ll need

  • WebStorm

Understand your first OpenAPI specification document

We are going to discover what the OpenAPI specification is through a simple example (inspired from OpenAPI planets). Remember that a RESTful API is about manipulation of representations of Web resources. In our example, we wish to provide an RESTful API that represents the planets of our solar system. So the resources are the planets.

Before going into the specifications of our API, we first need to introduce the format used for specifying the API. The OpenAPI specification supports two formats, namely JSON and YAML. Both languages are related to each other, since YAML is a superset of JSON.

The JSON format

A full description of the format can be found under JSON specifications and an introduction to the language is available under JSON introduction.

JSON stands for JavaScript Object Notation and it is a data-interchange format that humans can easily read and that computers can easily parse. For programmers, the JSON format can be easily handled, especially when using the JavaScript programming language - although it is fully language independent.

JSON is built on two universal data structures:

  • A collection of name/value pairs, called an Object. An object begins with { and ends with }. Each name is followed by : and the name/value pairs are separated by ,.
  • An ordered list of values, called an Array. An array is an ordered collection of values that starts [ and ends with ]. Values are separated by ,.

Values in an object or in an array can be another object or array, but it can also be an instance of a primitive type (i.e. a string, a number, a boolean (true or false), or the value null).

It is useful to give a few examples for a better understanding of the JSON syntax. Say that we want to represent all students participating in a classroom. In JSON, we would represent this as:

students.json
{
    "students": [{
        "name": "Bob",
        "email": "Bob.Marley@edu.hefr.ch"
      },
      {
        "name": "John",
        "email": "John.Lennon@edu.hefr.ch"
      },
      {
        "name": "Steve",
        "email": "Steve.McQueen@edu.hefr.ch"
      }
    ]
  }

In this example, students are represented with a name/value pair, where the name is “students” and the value is an array of representations of students by their name and email address.

You can validate that this representation is correct by using a JSON linter such as JSON linter. You may try to write other JSON representations and to validate them using a linter.

It is important to note that the JSON format is almost identical to what JavaScript objects are, with the restriction that in JSON names in name/value pairs must be strings, written with double quotes, while keys in JavaScript objects can be strings, number or identifier names.

Exercises to practice using JSON

A number of exercises to practice using JSON are suggested as EduTools tasks. If you are not very familiar with JSON or using JSON in JavaScript, it is probably a good idea to spend some time doing these exercises.

A note on the YAML format

The YAML format also aims to be a human readable data interchange format, with different priorities as compared to the JSON format. JSON’s foremost design goal is simplicity and universality, for trivial generation and parsing, at the cost of reduced human readability. In contrast, YAML’s foremost design goals are human readability and support for serializing arbitrary native data structures. Thus, YAML allows for extremely readable files, but is more complex to generate and parse.

YAML can be viewed as a natural superset of JSON, offering improved human readability and a more complete information model. This is also the case in practice; every JSON file is also a valid YAML file. This makes it easy to migrate from JSON to YAML if/when the additional features are required.

YAML can also be used as the format for specifying a RESTful API using the OpenAPI specification. For the sake of simplicity, we will however restrict ourselves to using the JSON format.

The OpenAPI document

An OpenAPI document is a text file representing a JSON object, in either JSON or YAML format. The document is commonly called “openapi.json” or “openapi.yaml”, but it can be named differently. This document contains the definition of a single JSON object containing fields adhering to the structure defined in the OpenAPI Specification (OAS).

The root object in any OpenAPI document is the OpenAPI Object and only three of its fields are mandatory: “openapi”, “info” and “paths”. The “openapi” field indicates the version of the OAS that this document is using (e.g. “3.1.0”, the “info” field contains a title and a version number (both as string), and the “paths” field contains all endpoints of the API. An example of thus a minimal document is

minimal_openapi.json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Planets API",
    "description": "Simple API delivering information about planets",
    "version": "0.0.1"
  },
  "paths": {}
}

In this example, the “paths” field is empty meaning that no endpoint exists. But, of course, any RESTful API will provide some endpoints.

The RESTful API Endpoints

The RESTful API endpoints or “paths” field describes all operations supported by the API, as defined in Paths Object. The “paths” field holds the relative paths to the individual endpoints, as well as their operations. Each endpoint is thus described by a name/value pair where the name describes the path and the value is a Path Item Object that describes the operations available for the endpoint. The Path Item object contains a field defining each possible operation for the endpoint and this field is made of a name/value pair where the name is the HTTP verb (e.g. “get”) and the value is an Operation Object. The Operation object describes the operation’s parameters, payload and possible server responses.

As an example of the definition of an endpoint, let us complete the definition of our Planet RESTful API as given above. Say that our API delivers the list of all planets on the “/planets” endpoint, a possible definition of such an API would be

planets_api.json
{
  "openapi": "3.0.3",
  "info": {
    "title": "Planets API",
    "description": "Simple API delivering information about planets for demonstrating the OpenAPI specification",
    "version": "0.0.1"
  },
  "paths": {
    "/planets": {
      "get": {
        "operationId": "allPlanets",
        "summary": "List all planets",
        "description": "Returns a list of all the planets of our solar system",
        "tags": [
          "planets"
        ],
        "responses": {
          "200": {
            "description": "Planets in a list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "./schemas/planet_in_list_schema.json"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

From this definition, we may understand the following:

  • Our Planets RESTful API contains one endpoint with the path “/planets”.
  • On this endpoint, one operation is possible: “get”. This operation is defined with an Operation object that contains several fields:
    • A “summary” and a “description” field that helps the API users to understand what the operation does.
    • An “operationId” field that may be used by OpenAPI tools such as code generators. The “operationId” value is a string that must be unique in the entire API and that is case sensitive.
    • The “tags” field contains a list of strings that are used for API documentation control.
  • The Operation object also contains a “responses” field, which is made of a list of Responses Object, each being a HTTP status code/Response Object pair. In the example above, the response with the “200” HTTP status code is documented with the Response object that contains the following fields:
    • A “description” field that helps the API users to understand what the response content is.
    • A “content” field that contains a map with descriptions of potential response payloads for different media types. In our example, the content is delivered as JSON content that is defined by a Schema Object. This Schema object is defined using a relative reference $ref - not yet part of the OpenAPI document.

Defining the payload content using a Schema Object

A Schema object allows the definition of input and output data types. These types can be objects, but also primitives and arrays. The Schema object is a superset of the JSON Schema Specification Draft 2020-12.

The understanding of the specification is beyond the scope of this codelab and we will limit our understanding to simple models through a few examples. If the payload content is a simple string defining an e-mail address, then the Schema object for this payload may be defined as

email_schema.json
{
  "type": "string",
  "format": "email"
}

If the content payload is a JSON object with several properties describing a customer, then the payload may look like

customer_schema.json
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "customer name"
    },
    "email": {
      "type": "string",
      "description": "customer email",
      "format": "email",
      "example": "john.doe@example.com"
    }
  }
}

For our Planets API, we may describe a planet in the list by its name and its position in the solar system (order in place from the sun). This leads to the following definition of the Schema object for an item of the array defining the list of planets:

schemas/planet_in_list_schema.json
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Name of planet",
      "example": "Earth"
    },
    "position": {
      "type": "number",
      "description": "Order in place from the Sun",
      "example": 3
    }
  }
}

You need to add a new file named “planet_in_list_schema.json” in a “schemas” folder with this content for a correct definition of your OpenAPI document.

Completing the specification document

A few more definitions are still missing in our OpenAPI document. An important one is the definition of the servers that implements the RESTful API and that can for instance be used for testing. It is important to understand that the “path” fields as described above hold the relative paths to the individual endpoints and their operations. These relative paths are appended to the URL from the “server” field in order to construct the full URL. The “server” field name contain an array of Server Object, which represents one or multiple servers. For one server, the Server object looks like

server_object.json
{
...
  "servers": [
    {
      "url": "http://localhost:5000",
      "description": "Sample server running locally"
    } 
  ]
...
}

The “url” field in this definition is mandatory. Additional server definitions can be added to the array of servers

Once you added the “servers” field to your OpenAPI document, it is functional and may be used in tools allowing visualization, testing and code generation, as described in the next steps.

Installing and Using WebStorm

For installing WebStorm, you need to follow the guide under Install WebStorm | WebStorm. You may install WebStorm as a standalone installer or using the IntelliJ Toolbox application. Please note that WebStorm is free when used for educational purposes, but you may need to create an IntelliJ account.

The OpenAPI Specifications JetBrains Plugin is now bundled with WebStorm. You may thus immediately create a new project and add a new OpenAPI specification file as shown below:

New OpenAPI specification

New OpenAPI specification

You must choose “OpenAPI 3.0 (.json)” as format for the specification document and name your document “planets_api”. The following document should be created for you:

planets_api.json
{
  "openapi": "3.0.3",
  "info": {
    "title": "Title",
    "description": "Title",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https"
    }
  ],
  "paths": {
  }
}

Fill the OpenAPI specification document

You may now modify and complete the document based on the previous step, by replacing and copying all required fields. Leave the OpenAPI version to be “3.0.3”, since version “3.1.0” is not yet supported by the plugin. Once this is done, you should be able to parse through the API specifications as shown below:

OpenAPI specification in Webstorm

OpenAPI specification in Webstorm

You may show or hide the Editor|Preview panels by selecting the preferred view in the upper right side of the window.

As you can observe, the preview panel lets you understand what the RESTful API endpoints are, what operations are supported on the endpoints and what the schemas of the response payload is. In the preview panel, you may also test specific operations, as the GET operation on the “/planets” endpoints. If you try to execute the operation, you should observe an error, since no server is implemented yet on the localhost.

Test the API on an existing server

The server http://appint03.tic.heia-fr.ch:8080 implements the Planets API on port 8080. You may thus add a Server object in the servers field. By doing so, you should then be able to select this server and test the API correctly. You should get the following response:

Response for requests to "/planets"

Response for requests to "/planets"

Once you successfully tested the “/planets” path, you may augment the API with the additional functionality of delivering information about a specific planet. The path for delivering this information is of the form “/planets/{planetId}”, where planetId is the planet position as received in the list. In addition to what was done for the “/planets” path, for specifying this path correctly you need to consider that it contains a parameter named planetId that is in the “path” field itself. In our example, we must specify that it is a number part of the path.

The definition of a parameter is done with a Parameter object, which contains the following field

  • “name” as the name of the parameter (required field),
  • “in” as the location of the parameter (required field). The value of this field is “path” in our example, since the parameter is specified in the path (and not as query parameter).
  • “required” specifying whether the parameter is required. In the case of a parameter located in the “path” field, the required field is required and must be set to true.
  • “schema” that defines the type used for the parameter. In our case, the type used is a number.

The “parameters” field of the “/planets/{planetId}” field is thus

planetId_parameters.json
"parameters": [
  {
    "name": "planetId",
    "in": "path",
    "required": true,
    "schema": {
      "type": "number",
      "example": 3
    }
  }
]

Using this definition and based on the definition of the “/planets” path, you may now fully define the “/planets/{planetId}” field. Note that the payload delivered for this request is similar to the one delivered for the “/planets” path, with the following differences:

  • The payload contains the definition for one planet only and is thus not an array with multiple items.
  • The planet schema contains in addition two fields which describe the number of confirmed and provisional moons that belong to the planet, as documented by NASA planet moons.

Once your definition is complete, you may again test it by querying the “appint03.tic.heia-fr.ch” server. You should obtain something similar to the results shown below. You may also try to set an invalid “planetId” parameter like 0 or 10 and observe the result. When the “planetId” parameter is invalid, you should get an error. When it is valid, you should get a detailed description of the planet. The way that errors are specified in errors in the OpenAPI specification document will be presented in a following codelab.

Response for requests to "/planets/5"

Response for requests to "/planets/5"

First Step in Specifying your own API

Now that you have understood and specified the Planets RESTful API using the OpenAPI specification and tools, you may start specifying the RESTful API that you will use for the course assignements. You are free to design your own API, but you must follow the following guidelines:

  • Your API should contain two endpoints.
  • One of the endpoints must support at least two query parameters in the request uri. Query parameters must be optional.
  • At least one of the responses delivered by the API must be an array of objects.
  • The format of each object delivered by the API is specified as a schema in a separate file.
  • The implementation of the API is based on data stored in a csv file.

The COVID-19 API as an example

We will demonstrate how to specify your own API using the COVID-19 API. This API is about providing data for the COVID-19 cases in Switzerland.

The COVID-19 API contains one endpoint used delivering the number of positive cases for a given time period and for each canton. When queried on this endpoint, the server delivers for each canton a payload describing the total number of positive cases for the entire period and for every day of the time period. An example of the response received is shown below:

Response for requests to "/positives"

Response for requests to "/positives"

From this example, one can observe that :

  • The start and end dates of the time period are specified as query parameters in the request uri.
  • The response is an array of objects. Each object in the array contains the name of the Canton and an array describing the number of positive cases for each day of the queried time period.

For specifying query parameters in the OpenAPI specification, one needs to add a Parameter object to the definition of the “path” field, similarly as what was done in a previous step for the “planetId” field. In this case, the “in” property of the parameter must define the parameter to be a “query” parameter and the parameter must be specified to be optional. This means that the request can be done on this endpoint without query parameters, in which case the request is done for the entire time period delivered by the server.

Important Rules when Specifying RESTful APIs

When designing Web APIs, there are two important questions that make a RESTful API and a RPC-like API different:

  • The first question is how the client conveys its intentions to the server. A RESTful API conveys this information with the HTTP method. Recall that the five most common HTTP methods are GET, HEAD, PUT, DELETE, and POST. So, with a RESTful API, the intention is given by the HTTP method itself: for instance, if the client wants to get information about a specific resource, then it will use the GET method. This is not the case with a RPC-like API that very often uses a single endpoint with a POST method. In this case, the intention is part of the envelope (e.g. the entity-body).
  • The other big question web services answer differently is how the client tells the server which part of the data to operate on. For instance, given that the server understands that the client wants to delete some data, how can it know which data the client wants to delete? In other words, the client needs to specify the scope of the request. A RESTful API specifies the scoping information of the request in the URI, while a RPC-like API puts the scoping information again into the entity-body.

As a recall, REST stands for Representational State Transfer. So a RESTful API is about the manipulation of representations of Web resources using a uniform set of stateless operations. At the core of any RESTful API are resources. {{ rest }} is thus not an architecture but it is rather a set of design criteria that should be respected:

  • It provides a uniform interface, meaning that the method information is kept in the HTTP method.
  • It is stateless: statelessness means that every HTTP request happens in complete isolation. When the client makes an HTTP request, it includes all information necessary for the server to fulfill that request.
  • It is cacheable: this is achieved by making the application addressable, exposing all interesting aspects of its data set as resources through an URI.

There are other rules that should be followed when designing a RESTful API, but these are the most important ones.

You may find some exercises related to OpenAPI here.