OpenAPI 3 with Go

baris bakla
8 min readJun 18, 2024

--

This guide will help use how to use Swagger in your Go applications. I will demonstrate it using alternative libraries. As a bonus, I will show how to integrate Swagger UI into the application. You can find the source code here

The design-first approach is quite valuable, and OpenAPI helps us with it. How can we apply it to our Go code? Several open-source projects assist us in the Go world. OpenApiTools also has a code generator. In this article, I will go through that code generator and a popular open source project, oapi-codegen. Additionally, having Swagger-UI in our application is also useful understanding the endpoints and interacting with the application for testing purposes. I will use a basic user-api spec as an example. Lets get our hands dirty!

Code generation using oapi-codegen

oapi-codegen uses kin-openapi project parsing and validating the spec file. It generates the entire model and server code for us, leaving us to implement the handler functions. I will explain the generated code in more detail but let me explain first how to install the dependency:

  1. Create your go project and create your mod file.
  2. Install the tool: go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest. Ensure that GOPATH/bin is added to your system’s PATH.
  3. Create the directory where you will put the generated code. The directory name in the example below is api.
  4. Run the command below to generate the code. user.gen.go file will be created under api folder.
oapi-codegen -package=api -generate "types,server,spec" user-app.yaml > api/user.gen.go

5. The code is now generated under api directory. You will see an interface called ServerInterface which you will implement later. We will come back to that later.

6. You may need to install some dependencies used by the the generated code. Alternatively, you can use tool.go approach.

Features

Use 3th party frameworks for server code

You can generate the server-side code for several other web framework libraries such as echo , gin , chi and some others. You can find the complete list of supported frameworks in the project’S README. The command for generating server-side code for gin is shown below. As you can see you, you provide gin as parameter in the prompt.

oapi-codegen -package=api  -generate gin user-app.yaml > api/todo.gen.go

Generate code in separate files

The all code we generated above for endpoints and the types is all in one Go file, which is not very readable. Lets make it better by separating the server code and models into different files.

  • Lets first create the server code:
oapi-codegen -package=api  -generate gin user-app.yaml > api/todo-server.gen.go
  • Lets now create the models:
oapi-codegen -package=api  -generate types user-app.yaml > api/todo-type.gen.go
  • If you want to create the client code or spec related code, here are the commands:
oapi-codegen -package=api  -generate client user-app.yaml > api/todo-client.gen.go
oapi-codegen -package=api  -generate spec user-app.yaml > api/todo-specs.gen.go

Using config files

As we add more parameters to the command, it can become quite verbose. Instead, we can define a config file and use it to manage boilerplate code.

Here is an example configuration for the non-strict server-side code:

package: oapi-codegen
output: oapi-codegen/server.gen.go
generate:
embedded-spec: false
strict-server: false
models: false
#chi-server: true # compatible with net/http
gin-server: true

The command for the server-side code would be:

oapi-codegen --config=server-cfg.yaml  user-app.yaml

A basic configuration file for model generation can be found here. I also have the command in the Makefile.

There are several other configuration properties. his example is just a simple demonstration. More configuration options can be found here.

Code generation using openapi-generator

We could also use OpenAPI Generator’s CLI tool. You can see how to install it here. After installing, run the command below. The code will be generated in the open-api directory. You will see that the generator creates each model and the api separately, making it quite easy to follow.

openapi-generator generate -g go-server -o open-api -i user-app.yaml

Features

Use 3th party frameworks for server code

You can also generate the code compatible with echo or gin. You can find more information here. Lets generate it for gin:

openapi-generator generate -g go-gin-server -o open-api -i user-app.yaml

Using config files

It is also possible to define a config file to simplify the command prompt. The list of command options for gin server can be found here. The basic command to execute the generation with config file is below. You can also find more fined-tuned command in the Makefile.

openapi-generator generate -c openapi-generator-cfg.yaml -i user-app.yaml

However, there are some catches with openapi-generator:

  1. Config options differ based on the server generator. Check here for the config options of your specific server generator.
  2. Generated main.go and go.mod files override your original main.go and go.mod files unless you define the output directory with -o parameter. It is also not possible to change it through config options. It can be frustrating, so be careful.
  3. The generated code is a separate go module. So I used a workspace to import that module into my own code.

Implementing the handlers

So we have generated the code! How will we use them with our own code?Note that I will assume that we generated the code using the configuration files.

oapi-codegen

You can generate the server-side code in either strict-server or non-strict-server mode. The strict-server mode generates more type-safe approach. You can enable/disable it in the configuration as below:

strict-server: false

The commands generating codes in strict-server or non-strict-server modes can be found in the Makefile . Note that I have defined different configuration files for each mode in the sake of tidiness.

In either case, the generated code contains an interface ServerInterface that includes handler functions for each endpoint. The function names are derived from the value of the operationId field in the spec. Note that I didn’t specify an operationId to the PUT request, so an automatic name PutApiV1UsersUserId was assigned, which is not very readable. So I would recommend assigning a meaningful operationId.

The struct ServerInterfaceWrapper, which contains aServerInterface instance variable, is also common in both strict and non-strict server mode. ServerInterfaceWrapper also implements the ServerInterface. All the handler functions of ServerInterface injected into ServerInterfaceWrapper as instance variable are called by the receiver functions of ServerInterfaceWrapper .

All the endpoints are registered to the router by RegisterHandlersWithOptions. RegisterHandlersWithOptions calls the handler functions of ServerInterfaceWrapper which are actually instances of ServerInterface.

In this case, all we have to do is implement ServerInterface, create a new ServerInterfaceWrapper with our implementation and call RegisterHandlersWithOptions. That is the standard implementation of the nonstrict-server case. I will come to that later.

non-strict-server

I created a dummy user-service.go that behaves as a mock database.

As I explained above, the router functions call wrapper handling functions, and these wrapper handling functions call the ServerInterface handlers and middleware functions. Therefore, we have to implement ServerInterface and inject it into ServerInterfaceWrapper along with oany middleware functions we have. We can then call RegisterHandlers or RegisterHandlersWithOptions function without wrapper. Alternatively we can directly call the register functions with our implementation without the wrapper. However, this approach doesn’t allow us to execute middleware functions. Below is example source code, which can also be found here.

server := not_strict_server.NewServer()
router := gin.Default()
not_strict_server.RegisterHandlers(router, server)

s := &http.Server{
Handler: router,
Addr: "0.0.0.0:8080",
}
log.Fatal(s.ListenAndServe())
  1. NontStrictServer struct that implements the ServerInterface.
  2. RegisterHandlers calls RegisterHandlersWithOptions , which creates a wrapper and creates the routes.

strict-server

I created a dummy user-service.go that behaves as a mock of an database.

In addition to ServerInterface , strict-server configuration also introduces another interface called StrictServerInterface . This interface also derives its function names from operationId field of the endpoints defined in the specification. The only difference between StrictServerInterface and ServerInterface lies in the signature of the functions. For instance, ServerInterface contains CreateUser(c *ginContext) while CreateUser(ctx context.Context, request CreateUserRequestObject). As you can see strict-server configuration enhances type safety.

There is another struct strictHandler, which also implements ServerInterface and owns StrictServerInterface instance variable. strictHandler acts a wrapper around ServerInterface. Why do we need StrictServerInterface? It enhances the type safety by automatically creating the request and response objects. This saves us from duplicate code retrieving request bodies and parameters.

Here is the declaration of strictHandler:

type strictHandler struct {
ssi StrictServerInterface
middlewares []StrictMiddlewareFunc
}

Since it already implements ServerInterface we can directly use it in our application, right?! Additionally, the NewStrictHandler function privided in the generated code simplifies the instantiation. The generated code of NewStrictHandleris below.

func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface {
return &strictHandler{ssi: ssi, middlewares: middlewares}
}

I implemented StrictServerInterface here. All we have to do is, to create a new strictHandler and register our own implementation to the router. The code example is below and can also be found here:

server := strict_server.NewServer()
router := gin.Default()
addSwaggerEndpoint(router)
sh := strict_server.NewStrictHandler(server, nil) // NewStrictHandler is auto-generated
strict_server.RegisterHandlers(router, sh)

s := &http.Server{
Handler: router,
Addr: "0.0.0.0:8080",
}
log.Fatal(s.ListenAndServe())

openapi-generator

openapi-generator generator an interface for each api group. We have only user group in our simple design. So we have only api_user.go generated for us.

We have also a struct ApiHandleFunctions which takes the interface defined in our api_user.go file.

type ApiHandleFunctions struct {

// Routes for the UserAPI part of the API
UserAPI UserAPI
}

Have a look now on routers.go. This function utilizes ApiHandleFunctions as a parameter in its signature. Contrary to its name, getRoutes doesn't simply retrieve routes but instead constructs them. Each endpoint defined within getRoutes invokes a method from the UserAPI interface. The naming convention used here may seem misleading since getRoutes implies route retrieval rather than creation. However, in practice, it serves to define routes based on the provided API functions.

func getRoutes(handleFunctions ApiHandleFunctions)  []Route {}

But wait! getRoutes is private! So who calls getRoutes function? We have another struct Router and a function NewRouter that creates a Router for us. That router struct then creates the routes. Finally, we are ready to go!

So all we have to do is:

  1. Implement UserAPI.
  2. Create ApiHandleFunctions struct
  3. Create our Router via NewRouter.

The example code can be found here.

Our main function would be:

routes := openapigengin.ApiHandleFunctions{UserAPI: openapigenonlyinterface.NewUserAPI()}
log.Printf("application with openpiGenerator boilder code started")

router := openapigengin.NewRouter(routes)
log.Fatal(router.Run(":8080"))

Show Swagger UI

We will host the swagger release in our project and serve the filesystem via an endpoint. Lets start:

  1. Download the latest release
  2. Unzip the file and you will see the dist folder. Copy it to your project folder and rename it swagger-ui. Sure, you dont need to rename it.
  3. Copy your api spec under swagger-ui folder and modify the url parameter in swagger-initializer.js pointing your local open api spec.
  4. Create endpoint to call the swagger ui. Note that go:embed helps us to embed the content of swagger-ui into the exe file.
//go:embed swagger-ui
var swaggerContent embed.FS

func main() {

// Define the router and other paths
fsys, _ := fs.Sub(swaggerContent, "swagger-ui")
router.StaticFS("/swagger", http.FS(fsys))
}

5. Restart your application and call http://localhost:8080/swagger. You have your swagger-ui!

--

--