Ваш IP: Нет данных · Статус: ЗащищеноНезащищенНет данных

Перейти к основному содержимому

Tips for tool-building using OpenAPI

The main reason I became a developer was to build interesting things. But as with every job, you quickly realize that repetitive tasks are a big part of your life. Fortunately, as developers, we are in a unique position to create tools that either reduce or completely eliminate those repetitive tasks.

Tips for tool-building using OpenAPI

One such task for us at Nord Security is to create client SDKs for the APIs and keep them up to date.

What’s the problem with client SDKs?

Let’s say we build a simple gamer API that would allow gamers to register:

POST /api/v1/gamers

We then create a php-gamer-sdk client so that other projects could use it. After some discussion with the other team, you find out that we need a golang sdk as well. Easy, you create a go-gamer-sdk for everyone to use.

Then you receive a message from the nodejs gang: why is there no client for the almighty typescript? Well, they do have a point! Let’s add yet another client – js-gamer-sdk.

Oh, shoot! We forgot to add an endpoint for retrieving a gamer by uuid!

GET /api/v1/gamers/{uuid}

Now comes the tedious and lengthy task of updating all the client-SDKs. All the developers now need to know 3 languages to be able to maintain the codebase.

An alternative to manual SDK updates

Instead of creating the SDKs manually, we can use OpenAPI. OpenAPI is a standard for describing APIs using plain old YAML files. This standard (previously known as Swagger) uses a format that can be read by both humans and machines.

How to get started

Every OpenAPI file starts with the basic info:


    openapi: 3.0.0
info:
  title: Gamer API
  description: An API for creating and managing gamers
  version: 0.1.0
servers:
  - url: https://gamer-api.pci.test/api/v1
        description: Local development server
        

The first line defines the version of the standard used. We then add additional info about the microservice, including the servers where you can access our API.

Now it’s time to actually describe the endpoints.


    paths:
  /gamers:
    post:
      operationId: createGamer
      summary: Create a gamer
      responses:
        '200':
          description: Gamer creation succeeded!
        '500':
          description: Unexpected issue detected, try again later.
          

We first describe the path of our endpoint as `/gamers`, and the method that is used to access it as `post`. After that, you assign a name to this endpoint via `operationId` and give a brief description in the `summary` field. Last but not least, you specify what response codes could be returned by the API.

One thing is missing though: there is no info on what kind of data the API receives to create a gamer.

Let’s fix that in the next section.

Accepting request parameters

Describing incoming data is pretty easy using the requestBody property.


    paths:
  /gamers:
    post:
      operationId: createGamer
      summary: Create a gamer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                # define all your json properties you will pass in the request
                

We want to show that the request parameters are non-optional, so we mark the content as required. Note that we will pass JSON to the API endpoint, which is why we tell OpenAPI that the request must be in application/json format.

Now comes the important part: we describe the type of the request data. There are many types to choose from, but for this example, we will describe the data as an object. Now all that’s left is to define its properties.


    paths:
  /gamers:
    post:
      operationId: createGamer
      summary: Create a gamer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nickname:
                  type: string
                  example: 'monkKey'
                clan:
                  type: string
                  example: 'chillpill'
        

As you can see, you can add examples of the fields you can pass. For the gamer to create an endpoint, the following object can be passed:


    {
  "nickname": "monkKey",
  "clan": "chillpill"
    }
    

The same approach can be used to describe responses


      responses:
        '200':
          description: Gamer creation succeeded!
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    format: uuid
                    example: 'e5159ff7-9e12-11ec-ad6c-2cdb0742b873'
                  nickname:
                    type: string
                    example: 'monkKey'
                  clan:
                    type: string
                    example: 'chillpill'
                    

As you can see, the response of our API is almost identical to the request, except for an additional `uuid` field is returned. Note the `format` property that servers as a hint to the content of the property.

Judging from the current OpenAPI file, the response is as follows:


{
  "uuid": "e5159ff7-9e12-11ec-ad6c-2cdb0742b873",
  "nickname": "monkKey",
  "clan": "chillpill"
}
    

Here’s what we have so far:


openapi: 3.0.0
info:
  title: Gamer API
  description: An API for creating and managing gamers
  version: 0.1.0
servers:
  - url: https://gamer-api.pci.test/api/v1
    description: Local development server
paths:
  /gamers:
    post:
      operationId: createGamer
      summary: Create a gamer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nickname:
                  type: string
                  example: 'monkKey'
                clan:
                  type: string
                  example: 'chillpill'
      responses:
        '200':
          description: Gamer creation succeeded!
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                    format: uuid
                    example: 'e5159ff7-9e12-11ec-ad6c-2cdb0742b873'
                  nickname:
                    type: string
                    example: 'monkKey'
                  clan:
                    type: string
                    example: 'chillpill'
        

Using references

Even though our specification file describes a simple endpoint, it already looks like a mess – it is pretty difficult to read. There is, however, an easy way around that – references.

The idea behind a reference is simple – you can define something elsewhere in the specification and point to it. It’s basically like defining a variable and using it later.

So first, define your request and response as reusable components.


    components:
  schemas:
    GamerCreateRequest:
      type: object
      properties:
        nickname:
          type: string
        clan:
          type: string
    GamerCreateResponse:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        nickname:
          type: string
        clan:
          type: string
        

Then, you can reference both request and response definition using the $ref keyword.


    paths:
  /gamers:
    post:
      operationId: createGamer
      summary: Create a gamer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GamerCreateRequest'
      responses:
        '200':
          description: Gamer creation succeeded!
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GamerCreateResponse'
                

Here I am referencing the schemas I defined earlier. As you can see, the file now looks much cleaner!

And with that, we are officially done with our specifications. Now, we can use it to generate things!

Generating a client

For client generation, we use an amazing tool called OpenAPI generator. This tool allows us to generate clients in a variety of languages. Let’s use it to generate a PHP client.

First, you need to save all the API definitions we have so far into a YAML file and pass the path to it.

$ openapi generate -i /path/to/specification.yaml

We then pass the folder where to put the generated code using -o

$ openapi generate -i /path/to/specification.yaml -g php -o ./gen/php

It’s also important to name the package properly, so that it could be added to package repositories. For that, --git-user* options a godsend.

$ openapi generate -i /path/to/specification.yaml -g php -o ./gen/php --git-user-id=nordsec --git-repo-id=gamer-sdk

And finally, you can pass a bunch of additional configuration that slightly tweaks the naming of the namespaces, folders and variables

 $ openapi generate -i /path/to/specification.yaml -g php -o ./gen/php --git-user-id=nordsec --git-repo-id=gamer-sdk --additional-properties=variableNamingConvention=camelCase,packageName=GamerApiClient,modelPackage=Dto,invokerPackage=NordsecGamer
    

And you’re done, the command can now be run! Check your gen/php directory for the generated source code.

The openapi generator not only creates a client, but data transfer objects for requests and responses as well. Here’s an example of how you could use the generated code:


$gamerCreateRequest = new GamerCreateRequest(); // NordsecPaymentsDtoGamerCreateRequest
$gamerCreateRequest->setNickname('rocket')
$gamerCreateRequest->setClan('rocketRoll')

$gamerCreateResponse = $apiInstance->createGamer($gamerCreateRequest);
echo $gamerCreateResponse->getUuid();

Protip: If you don’t want to setup an additional tool just to play around with OpenAPI, you can even use PhpStorm to generate code. Just open your OpenAPI file and use the icons at the top.

Open your API file

Generating documentation

Fortunately for us, generating SDKs for APIs is not the only benefit of OpenAPI. There are tools that use the specification file to generate API references. This is a cheap way to create simple documentation.

For that, we use widdershins. Here’s how you can quickly generate a slate-compatible documentation file:


    $ mkdir slate

    $ widdershins --search false --language_tabs shell php go javascript --summary /path/to/specification.yaml -o ./slate/index.html.md
    

This will generate documentation with code examples that are compatible with slate in the slate directory.

Documentation generation

Not only does this produce beautiful documentation, but it also doesn’t require any additional effort. Nice!

Using Postman

That wasn’t even the last possible use-case for the OpenAPI specification! OpenAPI can help you query your endpoints using postman. Instead of defining all endpoints yourself, use the import feature.

Navigate to File -> Import:

The Postman import window

Then choose what you want to use the specification file for. We are going to use it as a “Test Suite”:

Use the spec file as a test suite

And finally, we click import and enjoy the results.

The main postman window

OpenAPI – a powerful tool

Even though we’ve touched on quite a few usages of OpenAPI, we’ve barely scratched the surface. Because the format is readable by both humans and machines, more and more tools are created that take advantage of it. Therefore, OpenAPI is definitely a powerful tool for any API developer.