Learning Go by examples: part 11 - Generate a Go SDK (API client library) from your Go REST API

AurΓ©lie Vache - Jul 17 '23 - - Dev Community

In previous articles we created an HTTP REST API server and also a CLI.

Creating a CLI that directly calls our API is cool but a good practice is to have an abstraction (a SDK, a client library) between your users and your API. Creating an SDK from scratch is not as easy as you may think. What if I told you that it is possible to use a generator to create our SDK in Go from our REST API and more precisely from our Open API specifications?

mario gopher

Let's-a-go!

SDK

Let's start this article by talking a bit about API and SDK.

Having an API is cool but if you don't have SDKs (client libraries), that are abstraction layer between your users and your APIs, your users have to know by heart the architecture of your APIs and each time your APIs evolves, they need to change their applications on their side too.

SDK

You can also provide for your apps, infra, products, services several SDKs, one by programming language used by your communities.

OpenAPI Generator

If you search on the internet you will find that there are several tools that allow, on paper, to generate an SDK/a client library from a swagger file. Each tool has its pros and cons. For our SDK we will use the tool that came out the most during my research, is maintained, documented, has a hell of a list of target languages and has a ready-to-use CLI: OpenAPI Generator.

OpenAPI Generator

In short, OpenAPI Generator allows generation of API client librairies (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3).

With 50+ client generators, for a large variety of programming languages, it is possible to generate code to interact with any server which exposes an OpenAPI document.

The OpenAPI Generator is open sourced and have a GitHub repository.

In this article we will use the Go generator that will allow us to generate a Go SDK, with the parameters we will define.

Stop the blahblah, let's install the OpenAPI Generator.
Several installation options of the CLI exists, from sources, Docker, npm...

Let's install the CLI from brew:



$ brew install openapi-generator


Enter fullscreen mode Exit fullscreen mode

Let's check the CLI is correctly installed locally:



$ openapi-generator version
6.6.0


Enter fullscreen mode Exit fullscreen mode

What do we want?

Cool we have installed the generator but what do we want to do?

We have a nifty API Gophers API.

Gophers API

This simple API handle cute Gophers and alllows you to:

  • list the existing Gophers
  • display the information about a Gopher
  • create a new Gopher
  • delete a Gopher
  • update the path and the URL of a Gopher

What do you think of starting from this nice API and creating an SDK in Go? πŸ™‚

Initialization

First of all, we can create our repository in GitHub (in order to share and open-source it).

For that, I logged in GitHub website, clicked on the repositories link, click on "New" green button and then I created a new repository called β€œgophers-sdk-go”.

Now, in your local computer, git clone this new repository where you want:



$ git clone https://github.com/scraly/gophers-sdk-go.git
$ cd gophers-sdk-go


Enter fullscreen mode Exit fullscreen mode

Now, we have to initialize Go modules (dependency management):



$ go mod init github.com/scraly/gophers-sdk-go
go: creating new go.mod: module github.com/scraly/gophers-sdk-go


Enter fullscreen mode Exit fullscreen mode

This will create a go.mod file like this:



module github.com/scraly/gophers-sdk-go

go 1.19


Enter fullscreen mode Exit fullscreen mode

OpenAPI definition

We found a cool tool to generate a SDK from a swagger file/an OpenAPI definition so let's take a look to the specs definition of our Gophers API:



consumes:
- application/json
info:
  description: HTTP server that handle cute Gophers.
  title: gophers-api
  version: 0.1.0
produces:
- application/json
host: localhost:8080
schemes:
- http
swagger: "2.0"
tags:
  - name: gophers
    description: Handle Gophers

paths:
  /healthz:
    get:
      description: Check Health
      tags:
        - gophers
      operationId: checkHealth
      produces:
      - text/plain
      responses:
        '200':
          description: OK message.
          headers:
            Access-Control-Allow-Origin:
              type: string
          schema:
            type: string
            enum:
            - OK
  /gophers:
    get:
      description: List Gophers
      tags:
        - gophers
      produces:
       - application/json
      responses:
        200:
          description: Return the Gophers list.
          headers:
            Access-Control-Allow-Origin:
              type: string
          schema:
            type: array
            items:
              $ref: '#/definitions/Gopher'


  /gopher:
    post:
      summary: Add a new Gopher
      tags:
        - gophers
      consumes:
        - application/json
      parameters:
        - in: body
          name: gopher
          description: The Gopher to create.
          schema:
            type: object
            required:
              - name
              - displayname
              - url
            properties:
              name:
                type: string
              displayname:
                type: string
              url:
                type: string
      responses:
        201:
          description: Created
          schema:
            type: object
            $ref: '#/definitions/Gopher'
        409:
          description: Gopher already exists
    get:
      description: Get a gopher by a given name
      tags:
        - gophers
      produces:
       - application/json
      parameters:
        - name: name
          in: query
          type: string
          required: true
          description: Gopher name
      responses:
        200:
          description: A gopher
          headers:
            Access-Control-Allow-Origin:
              type: string
          schema:
            type: object
            $ref: '#/definitions/Gopher'
        404:
          description: A gopher with the specified Name was not found.
          headers:
            Access-Control-Allow-Origin:
              type: string
    delete:
      description: Delete a gopher by a given name
      tags:
        - gophers
      parameters:
        - name: name
          in: query
          type: string
          required: true
          description: Gopher name
      responses:
        200:
          description: OK
        404:
          description: A gopher with the specified Name was not found.
    put:
      description: Update a gopher
      tags:
        - gophers
      parameters:
        - in: body
          name: gopher
          description: The Gopher to update.
          schema:
            type: object
            required:
              - name
              - displayname
              - url
            properties:
              name:
                type: string
              displayname:
                type: string
              url:
                type: string
      responses:
        200:
          description: Updated
          schema:
            type: object
            $ref: '#/definitions/Gopher'
        404:
          description: A gopher with the specified Name was not found.

definitions:
  Gopher:
    type: object
    properties:
      name:
        type: string
        example: my-gopher
      displayname:
        type: string
        example: My Gopher
      url:
        type: string
        example: https://raw.githubusercontent.com/scraly/gophers/main/arrow-gopher.png


Enter fullscreen mode Exit fullscreen mode

Useful tips

In order for our SDK to be generated with the right information, we must be careful to define certain elements of the swagger file.
So we are going to follow the advice of our "Yoda Gopher" and see what are the tips to follow.

A host/server you will have

yoda gopher

By default, the generator thinks that the API is accessible in localhost, with no port defined, so please define a host or a server with the URL that allows access to your API. These fields are read by the generator.
Thanks to this, when a user/a developer will use your SDK, it will access the API directly.

On my side as you can see in the swagger file, I use the host field which is equal to localhost:8080.
So my API will have to run locally on port 8080 so that calls made through the SDK work.

Tags you will define

yoda gopher

If we do not define tags in our swagger file, the generated SDK will have the API name: DefaultApi. Personally, I don't find it great to have an SDK with DefaultApi. So we can specify tags and thanks to the tags, the generator will name your API(s) at your convenience.

For example, by defining and using the gophers tag, the generator will generate the API name GophersApi, which is much better than DefaultApi^^.

OperationId you will not forget

yoda gopher

Some generators, including the OpenAPI generator, use the operationId field to name the methods. Each operationId must be unique among all operation in the swagger file.

Check the validity of the specs

Before going any further, we can check if our swagger file is valid for the generator.



$ openapi-generator validate -i https://raw.githubusercontent.com/scraly/gophers-api/main/pkg/swagger/swagger.yml
Validating spec (https://raw.githubusercontent.com/scraly/gophers-api/main/pkg/swagger/swagger.yml)
No validation issues detected.


Enter fullscreen mode Exit fullscreen mode

Perfect βœ….

Configure the app

We are going to use a generator that will create and modify a whole bunch of files for us. In order to tell the tool to not modify or delete certain files, OpenAPI Generator supports a .openapi-generator-ignore file. It's like a .gitignore or .dockerignore file πŸ™‚.

Let's create a .openapi-generator-ignore file in order to tell to OpenAPI Generator to not generate or edit go.mod, go.sum and LICENSE files, because we already created them with go mod init command and the license file in GitHub:



# OpenAPI Generator Ignore

go.mod
go.sum
LICENSE


Enter fullscreen mode Exit fullscreen mode

Create our application

Let's use OpenAPI Generator CLI to generate our Go SDK:



$ openapi-generator generate \
  -i https://raw.githubusercontent.com/scraly/gophers-api/main/pkg/swagger/swagger.yml \
  -g go \
  --additional-properties packageName=gopherssdkgo,packageVersion=0.0.4,useTags=true \
  --git-user-id scraly \
  --git-repo-id gophers-sdk-go


Enter fullscreen mode Exit fullscreen mode

A good practice is to define all the parameters we want in order to generate the SDK according to our needs.

Let's explain what we defined:

  • -i (--input-spec) parameter allows you to define the source of your swagger/OpenAPI definition file so we gave the swagger file of our Gophers API
  • -g (--generator-name) parameter allows you to define what kind of SDK you want. Here we want a SDK in Go
  • --additional-properties parameter allows you to customize the generated Go SDK with your needs: the name of the package, the version of the package, if you want to take in account Swagger/OpenAPI tags we talked earlier ☺️...
  • By default the SDK will generate the import line like this: openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID". --git-user-id and --git-repo-id will allow you to customize it with your Git repository.

/!\ Don't use "-", "_" or non alphabetical character in the packageName field else you will have a strange error message when you will use your SDK πŸ˜…:



$ go run sample.go
# github.com/scraly/gophers-sdk-go
../../../../go/pkg/mod/github.com/scraly/gophers-sdk-go@v0.0.0-20230716090011-35a148834c43/api_gophers.go:11:16: syntax error: unexpected -, expected semicolon or newline


Enter fullscreen mode Exit fullscreen mode

For more information, you can take a look at the official documentation about package name in Go.

Please go to OpenAPI Generator documentation to know all the possible parameters for generate command.

Note that you can look at the Go generator documentation if you want to know more about all the possible parameters to configure and use.

Here the output you would have when you execute the command:



[main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: go (client)
[main] INFO  o.o.codegen.DefaultGenerator - Generator 'go' is considered stable.
[main] INFO  o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.AbstractGoCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as _gopher_put_request. To have complete control of the model name, set the `title` field or use the inlineSchemaNameMapping option (--inline-schema-name-mappings in CLI).
[main] INFO  o.o.codegen.TemplateManager - writing file ./model_gopher.go
[main] INFO  o.o.codegen.TemplateManager - writing file ./docs/Gopher.md
[main] INFO  o.o.codegen.TemplateManager - writing file ./model__gopher_put_request.go
[main] INFO  o.o.codegen.TemplateManager - writing file ./docs/GopherPutRequest.md
[main] WARN  o.o.codegen.DefaultCodegen - Empty operationId found for path: get /gophers. Renamed to auto-generated operationId: gophersGet
[main] WARN  o.o.codegen.DefaultCodegen - Empty operationId found for path: get /gopher. Renamed to auto-generated operationId: gopherGet
[main] WARN  o.o.codegen.DefaultCodegen - Empty operationId found for path: put /gopher. Renamed to auto-generated operationId: gopherPut
[main] WARN  o.o.codegen.DefaultCodegen - Empty operationId found for path: post /gopher. Renamed to auto-generated operationId: gopherPost
[main] WARN  o.o.codegen.DefaultCodegen - Empty operationId found for path: delete /gopher. Renamed to auto-generated operationId: gopherDelete
[main] INFO  o.o.codegen.TemplateManager - writing file ./api_gophers.go
[main] INFO  o.o.codegen.TemplateManager - Skipped ./test/api_gophers_test.go (Test files never overwrite an existing file of the same name.)
[main] INFO  o.o.codegen.TemplateManager - writing file ./docs/GophersApi.md
[main] INFO  o.o.codegen.TemplateManager - writing file ./api/openapi.yaml
[main] INFO  o.o.codegen.TemplateManager - writing file ./README.md
[main] INFO  o.o.codegen.TemplateManager - writing file ./git_push.sh
[main] INFO  o.o.codegen.TemplateManager - writing file ./.gitignore
[main] INFO  o.o.codegen.TemplateManager - writing file ./configuration.go
[main] INFO  o.o.codegen.TemplateManager - writing file ./client.go
[main] INFO  o.o.codegen.TemplateManager - writing file ./response.go
[main] INFO  o.o.codegen.TemplateManager - Ignored ./go.mod (Ignored by rule in ignore file.)
[main] INFO  o.o.codegen.TemplateManager - Ignored ./go.sum (Ignored by rule in ignore file.)
[main] INFO  o.o.codegen.TemplateManager - writing file /Users/aurelievache/git/github.com/scraly/gophers-sdk-go/./.travis.yml
[main] INFO  o.o.codegen.TemplateManager - writing file ./utils.go
[main] INFO  o.o.codegen.TemplateManager - Skipped ./.openapi-generator-ignore (Skipped by supportingFiles options supplied by user.)
[main] INFO  o.o.codegen.TemplateManager - writing file ./.openapi-generator/VERSION
[main] INFO  o.o.codegen.TemplateManager - writing file ./.openapi-generator/FILES
################################################################################
# Thanks for using OpenAPI Generator.                                          #
# Please consider donation to help us maintain this project πŸ™                 #
# https://opencollective.com/openapi_generator/donate                          #
################################################################################


Enter fullscreen mode Exit fullscreen mode

Cool, the SDK have been generated!

The command have generated useful files for you:



.
β”œβ”€β”€ LICENSE
β”œβ”€β”€ README.md
β”œβ”€β”€ api
β”‚Β Β  └── openapi.yaml
β”œβ”€β”€ api_gophers.go
β”œβ”€β”€ client.go
β”œβ”€β”€ configuration.go
β”œβ”€β”€ docs
β”‚Β Β  β”œβ”€β”€ Gopher.md
β”‚Β Β  β”œβ”€β”€ GopherPutRequest.md
β”‚Β Β  └── GophersApi.md
β”œβ”€β”€ git_push.sh
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ model__gopher_put_request.go
β”œβ”€β”€ model_gopher.go
β”œβ”€β”€ response.go
β”œβ”€β”€ sample
β”‚Β Β  └── sample.go
β”œβ”€β”€ test
β”‚Β Β  └── api_gophers_test.go
└── utils.go


Enter fullscreen mode Exit fullscreen mode

As you can see, the generator has also generated documentation files like README.md and docs folder that will help you to know how to use the fresh new SDK.

Now we can push the first version of our generated SDK in GO in GitHub.



$ git add .
$ git commit -m "feat: first version of the generated go sdk" *
$ git push


Enter fullscreen mode Exit fullscreen mode

Good practice

Yoda gopher

Again, let's listen to our Yoda Gopher πŸ™‚.

A good practice is to create a tag (and a release) every new version of your SDK. It will help users to go get the latest version (or wanted version) of your released/published SDK.

Let's test it

Ahh I like this moment where we will be able to test what we have created (finally generated ^^).

Run the API locally

First, as we saw, the Swagger/OpenAPI specs defined our Gophers API is running in localhost:8080 so we need to run it locally πŸ˜‰.

Clone the Gophers API repository:



$ git clone https://github.com/scraly/gophers-api.git
$ cd gophers-api


Enter fullscreen mode Exit fullscreen mode

As we defined our tasks in a Taskfile in order to automate our common tasks, like the previous articles, we just have to execute the task run command to start the API in localhost:8080:



$ task run
task: [run] GOFLAGS=-mod=mod go run internal/main.go
2023/07/16 11:53:35 Serving gophers API at http://[::]:8080


Enter fullscreen mode Exit fullscreen mode

Let's test our API. Yes, sorry, but I like to test each steps of a project ^^.



$ curl localhost:8080/gophers
[{"displayname":"5th Element","name":"5th-element","url":"https://raw.githubusercontent.com/scraly/gophers/main/5th-element.png"}]


Enter fullscreen mode Exit fullscreen mode

Use and test our SDK

Now we have a running and published/released Go SDK for our Gophers, let's create a little sample that will test 2 API calls:

  • /healthz
  • /gophers

Let's create a sample.go file with the following content:



package main

import (
    "context"
    "fmt"
    "os"

    gopherssdk "github.com/scraly/gophers-sdk-go"
)

func main() {
    config := gopherssdk.NewConfiguration()
    client := gopherssdk.NewAPIClient(config)

    // Check Health
    // When we call GophersApi.CheckHealth method, it return a string
    // equals to OK if the Gophers API is running and healthy

    health, healthRes, healthErr := client.GophersApi.CheckHealth(context.Background()).Execute()
    if healthErr != nil {
        fmt.Fprintf(os.Stderr, "Error when calling `GophersApi.CheckHealth``: %v\n", healthErr)
        fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", healthRes)
    }
    // response from `CheckHealth`: string
    fmt.Fprintf(os.Stdout, "Response from `GophersApi.CheckHealth`: %v\n", health)

    // Get Gophers
    gophers, gophersRes, GophersErr := client.GophersApi.GophersGet(context.Background()).Execute()
    if GophersErr != nil {
        fmt.Fprintf(os.Stderr, "Error when calling `GophersApi.GophersGet``: %v\n", GophersErr)
        fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", gophersRes)
    }

    // response from `GophersGet`: []Gopher
    if gophersRes.StatusCode == 200 {

        // Get and display all existing Gophers
        fmt.Println("Response from `GophersApi.GophersGet`:")

        fmt.Println("Number of Gophers:", len(gophers))

        for _, myGopher := range gophers {
            fmt.Println("DisplayName: ", *myGopher.Displayname)
            fmt.Println("Name:", *myGopher.Name)
            fmt.Println("URL:", *myGopher.Url)
        }

    }
}
```

In this file we:
* import the Go SDK ^^
* initiate our client with a new configuration
* call `GophersApi.CheckHealth` method that call `/healthz` route and display the result
* call `GophersApi.GophersGet` method that call `/gophers` route and display the list of returned Gophers

Let's test it:

```bash
$ go run sample.go
Response from `GophersApi.CheckHealth`: OK
Response from `GophersApi.GophersGet`:
Number of Gophers: 1
DisplayName:  5th Element
Name: 5th-element
URL: https://raw.githubusercontent.com/scraly/gophers/main/5th-element.png
```

Cool! We are using the Go SDK to call our API running in localhost:8080 (without knowing the architecture of our API)! πŸ™‚

## What's next

In this article we saw how to generate a Go SDK from a swagger file/OpenAPI specs.
But what happens when our swagger file changes?
An idea can be to automatically regenerate our SDK at every changes of our swagger file/OpenAPI spec changes.

As our API and SDK are hosted in GitHub, this automation can be fixed with GitHub actions πŸ™‚.

We can, for example, think of using the hook `workflow_dispatch`, which allows a change in one repo to trigger an action in a different repository.

## Conclusion

As you have seen in this article and previous articles, it's possible to create applications in Go: CLI, REST API... and also to use helpful tools that will assist us in creating even an SDK.

All the code of our app is available in: https://github.com/scraly/gophers-sdk-go

The documentation is also available: https://pkg.go.dev/github.com/scraly/gophers-sdk-go

In the following articles we will create others kind/types of applications in Go.

Hope you'll like it.
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .