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?
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.
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.
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
Let's check the CLI is correctly installed locally:
$ openapi-generator version
6.6.0
What do we want?
Cool we have installed the generator but what do we want to do?
We have a nifty 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
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
This will create a go.mod
file like this:
module github.com/scraly/gophers-sdk-go
go 1.19
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
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
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
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
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.
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
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
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
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 #
################################################################################
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
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
Good practice
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
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
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"}]
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.