Designing the Glue between Server and Client

Documenting APIs with Insomnia and Redoc using OpenAPI 3.0.4

Designing the Glue between Server and Client

When working on a project for three months and then abandoning it, documentation is a bit of a pain. But for anything more than that, documenting the software structure is important, especially when working in a team or when planning to work in a team someday. Even when working alone I don’t remember what I thought six months ago, which functions should be deleted and out commented code can be removed.

While I have written about documenting actual code with swimm.io in a recent post, now I will go into looking at a micro service from a networking perspective by designing and testing Application Programmable Interfaces or APIs. Actually the world today is run using APIs.

Writing software documentation I often do after the code is written, but for deciding how an API call should behave and which routes should exist, it makes sense to design an API first before writing any code. There are different conventions how an API should look like, e.g. OpenAPI, and then a file syntax like YAML (maybe JSON or XML would be possible as well) that describes how to specify an API according to OpenAPI standard. Then there are RESTful naming conventions for the URL routes. I know, but it sounds more complicated than those fancy abbreviations make it seem. Learning those conventions and standards enables you to harness the experience and mistakes of decades of development without having to reinvent the wheel.

While OpenAPI version 3.1.0 is released, the tool Insomnia, which helps us write a OpenAPI specification in YAML, does only support version 3.0.4, so we have to stay with that for now. The new version makes working with files in POST request a little bit easier.

At the end of this post we will have written a long list as YAML file from which we can generate a website that will be our documentation with redoc. Think of it as a communication tool between front- and backend developers (which both can be yourself). Using the tool Insomnia (alternative to Postman) gives a live preview of this documentation website and later we can test the actual API with it. This is more convenient than just using VSCode.

Bildschirmfoto-2022-08-17-um-15.38.17.png

So first let’s download and create a new API in Insomnia or if you decide for another way create an api.yml file and insert the following line (without the ...).

openapi: 3.0.4
...

The current version of OpenAPI is 3.1.0. Insomnia unfortunately doesn’t support that, that results in some inconvenient syntax when uploading files as POST request, so we are going with 3.0.4.

Next we are extending the document step by step until we developed a full API definition.

  1. Set general information
  2. Set API server URL
  3. Define routes for HTTP requests
  4. Add Parameters and Send Files
  5. Define expected responses
  6. Define a schema for an object
  7. Publish the documentation with Redoc
  8. Bonus: Dealing with CORS Error in Go Echo
  9. Conclusion

Set general information

So let’s start with adding general information.

...
info:
  title: Pantera API
  description: "This is a animal identification API. The goal is to implement this in Go."
  version: 0.0.1
  termsOfService: http://swagger.io/terms/
  contact:
    email: info@pantera.io
  license:
    name: Commercial
    url: http://localhost
...

title is a self assigned name of the API.

description should describe the purpose of the API, well duh, obviously. But don’t take this lightly. Before writing a single line of code it makes you think and clearly state your intentions. This serves as a guide which functions the API should provide and maybe even more important which functions it should not provide, because they are part of a different service. Doesn’t have to be perfect starting out though, it evolves over time.

Version serves to denote the Version of your API. The first digit 1.x.x usually indicates major changes, that can be incompatible with earlier versions. Often functions are set deprecated when updating to one major release and then phased of with the release after that.

The termsOfService link to a legal document that provides terms under which the API can be used. You might want to restrict use of an API only to your company or limit commercial use or enforce rate limits, actually I am not sure if this is done in the terms, but in any case rate limits should also be enforced in the server code.

contact allows to provide an email or other contact information to the person responsible for the API.

license let’s you choose a license like CreativeCommons, MIT.... and link to the terms with a url.

...
externalDocs:
  description: Find out more about Swagger
  url: http://swagger.io
...
...
tags:
  - name: Animals
    description: Provides a list of Animals with key datapoints and an image analysis tool to identify those animals.
  - name: Zoos
    description: Returns a list of Zoos
...

Set API server URL

Next we set the server base URL. In the beginning this can be you own computer and later it will be your domain pantera.io. You can define multiple URLs as well, one for production, one for testing. Now, I have set a version v1, but it might be better to set it in an Accept-Version header.

...
servers:
  - url: http://127.0.0.1:1324/api/v1
...

This was the warmup. Next we will define our actual API paths and how the possible requests (think GET, POST, PUT) as well as responses looks like.

Define routes for HTTP requests

/animals/imageanalysis

Our first path is lets say /animals/imageanalysis

...
paths: 
    /animals/imageanalysis:
        post:
...

Then we add a tag under which this path should be categorised like animals, which we have defined earlier, but you can set new tags, they just won’t have any description then.

...
       tags:
       - Animals        
...

The route has it’s own summary and description. There is also a optional field operationId which has to be unique and refers to the function name in server code that handles that route.

...
       summary: "Object detection for animals"
       description: "Accepts an image as multiform/form-data and returns coordinates of objects detected in that image"
       operationId: parseImageAnalysisResponse
...

and maybe let’s add another tag for information about zoos.

...  
    - name: Zoos
        description: Manage a list of Zoos
...

Add Parameters and Send Files

This is now the interesting part, a form with a file to upload. There is a simpler way, which is just uploading one file or image. We go for a way to upload multiple files and parameters using multiparty/form-data. It is an object with multiple properties, let’s call the first file1. Although we talk about an image, it is still of type string and has format binary. Then we set the encoding of the field file1 as contentType: image/png, image/jpeg. I think about this as a snippet that I can copy and paste.

    ...
    requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file1:
                  type: string
                  format: binary
            encoding:
              file1:
                contentType: image/png, image/jpeg`
    ...

This works so far, but for further reading you can look into format: base64and the general Content-Type: application/octet-stream.

If we could work with OpenAPI 3.1.0, then it will look a little clearer.

# OpenAPI v3.1
requestBody:
  content:
    multipart/form-data:
      schema:
       type: object
       properties:
        orderId:
          type: integer
        fileName:
          type: string
          contentMediaType: application/octet-stream

Now after discussing how a request looks like, let’s move forward to HTTP responses.

Define expected responses

The most famous HTTP response code will probably be 404 - not found. This isn’t the most common one, since most of the time the internet works, we get 200 - success. Those codes are grouped 1xx, 2xx, 3xx, 4xx or 5xx and a list of available HTTP responses can be found here on Wikipedia.

...
responses:
        "405":
          description: Invalid input
        "200":
           description: JSON Response
           content:
             application/json:
              schema:
                  $ref: "#/components/schemas/AnimalAnalysis"
...

Ok, so first we open the section responses:. Then we decide which codes we want to use, in this case 405 and 200. Then we use description: for a summary on when this response will occur. If it is successful, we return application/json, since this about an API that uses JSON, but could also use XML. If it were a website it would be text/html and an image is image/jpg, you can find more here. Now we define the structure of that JSON response under schema:. This can look like this

examples:
    application/json:
      id: 38
      title: T-Shirt
      image:
         url: images/38.png

for the following JSON

{
    id: 38
    "title": T-Shirt
    "image": {
        url: image/38.png
    }
}

For more involved types that should be defined as structs later, we can refer to a schema and define it in a different section. Since we might expect multiple animals in one image we might obtain an array, this is denoted by using items instead of just adding the reference to the schema.

application/json:
    schema:
      items:
        $ref: '#/components/schemas/AnimalObject'

This will result in an error, since that object isn’t defined yet.

Define a schema for an object

Now we have to define the object we referred to in the last section.

So we give the type a name Bands. It is of type object. Then we define its properties.

Properties also have a name column and a description. This column has again a type which could be number, string or object. The format further specifies the type of string which could be an xid or a date. If a string just has a few possibilities, you can define those as a list in enum. And last we give a sample value under example.

...
components:
   schemas:
     AnimalAnalysis:
      type: object
      properties:
        detection:
           type: string
           enum:
             - auto
             - manual
           description: "Describes if an animal was detected by the object detection algorithm or edited manual by the user."
        detectedAnimal:
          type: string
          example: "Lion"
        xid:
          type: string
          format: xid
          example: "9m4e2mr0ui3e8a215n4g"
        xMin:
          type: number
          format: int64
          example: 45
          description: x-Coordinate of upper left corner.
        yMin:
          type: number
          format: int64
          example: 20
          description: y-Coordinate of upper left corner.
        xMax:
          type: number
          format: int64
          example: 80
          description: x-Coordinate of lower right corner.
        yMax:
          type: number
          format: int64
          example: 40
          description: y-Coordinate of lower right corner.
...

And that’s how we define the response for our API.

A object detection microservice will not deliver the response in a format that is convenient for us for use in the client. We have to adapt the data first. Checkout my post about Relaying a Microservice JSON Response to the Client by Unmarshalling Go Structs on how to implement this in Go.

Finally, the full listing

openapi: 3.0.4
info:
  title: Pantera API
  description: "This is a animal identification API. The goal is to implement this in Go."
  version: 0.0.1
  termsOfService: http://swagger.io/terms/
  contact:
    email: info@pantera.io
  license:
    name: Commercial
    url: http://localhost
tags:
  - name: Animals
    description: Provides a list of Animals with key datapoints and an image analysis tool to identify those animals.
  - name: Zoos
    description: Manage a list of Zoos
servers:
  - url: http://127.0.0.1:1324/api/v1
paths:
  /animals/imageanalysis:
     post:
       tags:
       - Animals
       summary: "Object detection for animals"
       description: "Accepts an image as multiform/form-data and returns coordinates of objects detected in that image"
       operationId: parseImageAnalysisResponse
       requestBody:
          content:
            multipart/form-data:
              schema:
                type: object
                properties:
                  file1:
                    type: string
                    format: binary
              encoding:
                file1:
                   contentType: image/png, image/jpeg
       responses:
          "405":
            description: Invalid input
          "200":
           description: JSON Response
           content:
             application/json:
              schema:
                items:
                  $ref: "#/components/schemas/AnimalAnalysis"
  /animals:
    get:
     tags:
     - Animals
     responses:
       "200":
         description: ""
components:
   schemas:
     AnimalAnalysis:
      type: object
      properties:
        detection:
           type: string
           enum:
             - auto
             - manual
           description: "Describes if an animal was detected by the object detection algorithm or edited manual by the user."
        detectedAnimal:
          type: string
          example: "Lion"
        xid:
          type: string
          format: xid
          example: "9m4e2mr0ui3e8a215n4g"
        xMin:
          type: number
          format: int64
          example: 45
          description: x-Coordinate of upper left corner.
        yMin:
          type: number
          format: int64
          example: 20
          description: y-Coordinate of upper left corner.
        xMax:
          type: number
          format: int64
          example: 80
          description: x-Coordinate of lower right corner.
        yMax:
          type: number
          format: int64
          example: 40
          description: y-Coordinate of lower right corner.

Publish the documentation with Redoc

Redoc.ly is a service for hosting OpenAPI based documentations and make them accessible for third parties. They have a premium hosting service and an open source CLI tool. You can install it with

$ npm i -g redoc-cli

Then copy the YAML file we created above and store it under api.yml.

You can preview the documentation with

$ redocly preview-docs api.yml -p 8080   
  🔎  Preview server running at http://127.0.0.1:8080

and open a browser on this URL.

Bildschirmfoto-2022-08-17-um-15.36.51.png

If you upload the file to a GitHub repository you can use Redocly to host your documentation and also keep it updated when you commit changes to your repository.

Bildschirmfoto-2022-08-16-um-23.27.38.png

Bonus: Dealing with CORS Error in Go Echo

When you use Insomnia to access the API you can use the try function to make API calls, but not from a browser with Redocly for browser security reasons (CORS).

Bildschirmfoto-2022-08-17-um-14.14.53.png

To avoid this install the echo middleware

$ go get github.com/labstack/echo/v4/middleware

and then in the code add the following line after e := echo.New()

e.Use(middleware.CORS())

Conclusion

Great, I really appreciate staying for so long and if you made it this far that should be enough to design your own APIs. Leave a comment with any questions, suggestions, things not working or words of encouragement.