OpenAPI 3 with Go
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:
- Create your go project and create your
mod
file. - Install the tool:
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
. Ensure thatGOPATH/bin
is added to your system’s PATH. - Create the directory where you will put the generated code. The directory name in the example below is
api
. - Run the command below to generate the code.
user.gen.go
file will be created underapi
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
:
- Config options differ based on the server generator. Check here for the config options of your specific server generator.
- Generated
main.go
andgo.mod
files override your originalmain.go
andgo.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. - 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())
NontStrictServer
struct that implements theServerInterface
.RegisterHandlers
callsRegisterHandlersWithOptions
, 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 NewStrictHandler
is 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:
- Implement
UserAPI
. - Create
ApiHandleFunctions
struct - Create our
Router
viaNewRouter
.
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:
- Download the latest release
- Unzip the file and you will see the
dist
folder. Copy it to your project folder and rename itswagger-ui
. Sure, you dont need to rename it. - Copy your api spec under
swagger-ui
folder and modify theurl
parameter inswagger-initializer.js
pointing your local open api spec. - Create endpoint to call the swagger ui. Note that
go:embed
helps us to embed the content ofswagger-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!