Go and the OpenAPI Ecosystem
Introduction
When developing a production backend server in the Go language, one of the primary challenges almost all developers encounter first is the following:
How should API documentation be handled?
Upon some investigation, one realizes the benefit of creating documentation that adheres to the OpenAPI specification and naturally seeks libraries that integrate with OpenAPI. However, even after making this decision, another problem arises.
There are many OpenAPI-related libraries; which one should I use?
This document is a brief introduction to libraries written for Go beginners who are experiencing this situation. This document was written based on the end of 2024, and as the language ecosystem is always fluid, it is recommended to reference this while also keeping abreast of the latest developments.
Strategies of Libraries for OpenAPI
As you may already know, OpenAPI is a specification for clearly defining and documenting REST APIs. It defines API endpoints, requests, and response formats in YAML or JSON, which not only helps developers but also automates the generation of frontend and backend code, thereby reducing meaningless repetition and minimizing minor human errors.
To seamlessly integrate OpenAPI into projects, libraries in the Go ecosystem generally adopt the following three strategies:
1. Combining Go Comments into OpenAPI Specification Documents
One of the difficulties when developing APIs according to OpenAPI is that the actual documentation and the code implementing it exist in separate files at completely different locations. Consequently, it is surprisingly frequent to update the code but forget to update the documentation, or update the documentation but fail to update the code.
To illustrate with a simple example:
- If the logic for an API is modified in a file such as
./internal/server/user.go, - and the actual documentation exists in
./openapi3.yaml, one might accidentally overlook making the corresponding change. - If a Pull Request is submitted without recognizing this issue and reviewed by colleagues,
- the reviewers might not notice the changes in
./openapi3.yaml. This could lead to an unfortunate situation where the API specification remains unchanged, but the actual API implementation is altered.
Writing API documentation in the form of Go comments can mitigate this issue to some extent. Since the code and documentation are co-located, comments can be updated alongside code modifications. Tools exist that automatically generate OpenAPI specification documents based on these comments.
A representative project is Swag. Swag parses comments in Go code to generate OpenAPI 2 format documents. Its usage is straightforward: one simply writes comments above handler functions according to the format defined by each library.
1// @Summary Create user
2// @Description Creates a new user.
3// @Tags Users
4// @Accept json
5// @Produce json
6// @Param user body models.User true "User information"
7// @Success 200 {object} models.User
8// @Failure 400 {object} models.ErrorResponse
9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11 // ...
12}
When comments are written in this manner, the Swag CLI parses them to generate an OpenAPI 2 document. Typically, this process occurs during the CI pipeline, and the generated OpenAPI specification document is then deployed to a Git repository, as a final build artifact, or to a separate external API documentation management system for collaboration with other projects.
Advantages:
- Since comments are co-located with the code, the likelihood of discrepancies between the actual code and the documentation is reduced.
- Documentation can be performed simply and flexibly using only comments, without the need for separate tools or complex configurations.
- As comments do not affect the actual API logic, it is suitable for adding temporary features that might be λΆλ΄μ€λ¬μ΄ to publicly document.
Disadvantages:
- The readability of a single code file can decrease as the number of lines of comments increases.
- It may be challenging to express all API specifications in the form of comments.
- Since the documentation does not enforce the code, there is no guarantee that the OpenAPI documentation and the actual logic will always align.
2. Generating Go Code from OpenAPI Specification Documents
An alternative approach places the Single Source of Truth (SSOT) not in the Go code but in the documentation. This method involves first defining the OpenAPI specification and then generating Go code based on that definition. Because the API specification directly generates the code, it culturally enforces API design as a preliminary step in development. This sequential order, where API specification definition is the very first step, offers the significant advantage of proactively preventing situations where overlooked details are only recognized after development completion, necessitating widespread API specification changes and subsequent code modifications.
Representative projects adopting this approach include oapi-codegen and OpenAPI Generator. The usage is straightforward:
- Write a YAML or JSON document conforming to the OpenAPI specification.
- Execute the CLI.
- Corresponding Go stub code is generated.
- Then, only the detailed logic for individual APIs that this stub will use needs to be implemented directly.
The following is an example of code generated by oapi-codegen:
1// StrictServerInterface represents all server handlers.
2type StrictServerInterface interface {
3 // ...
4 // Returns all pets
5 // (GET /pets)
6 FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7 // ...
8}
The code generated by oapi-codegen, using the interface above as a medium, performs logic such as parsing query parameters, headers, and body, as well as validation, and then invokes the appropriate method declared in the interface. Users only need to implement the concrete type for this interface to complete the necessary work for API implementation.
Advantages:
- Since the specification is established first and development proceeds thereafter, it is advantageous for parallelizing work when multiple teams are collaborating.
- Code for repetitive manual tasks is automatically generated, leading to increased work efficiency while remaining beneficial for debugging.
- It is easier to ensure that the documentation and code representation are always consistent.
Disadvantages:
- There is a somewhat steep initial learning curve if one is unfamiliar with the OpenAPI specification itself.
- Since the structure of the API handling code is automatically generated by the project, it can be challenging to adapt when customization is required.
Author's comment. As of October 2024, the Go code generated by OpenAPI Generator enforces the entire project structure, not just the API logic. This results in a rigid project structure, generating code that is unsuitable for adding various functionalities required in a real production environment. For those adopting this method, I strongly recommend using oapi-codegen. The author uses oapi-codegen + echo + StrictServerInterface.
3. Generating OpenAPI Specification Documents from Go Code
When dozens or hundreds of individuals develop for the same server, an inevitable issue is the potential for inconsistency across individual APIs. As a straightforward example, if specifications for over 100 API endpoints are declared in a single OpenAPI YAML file, that file would become a monstrous entity exceeding 10,000 lines. Inevitably, new API endpoint declarations would lead to issues such as redundant model declarations, omission of certain fields, or the creation of path naming that violates conventions, thus compromising the overall consistency of the API.
To address such issues, one could appoint a separate owner to manage the OpenAPI YAML, or develop a linter to automatically catch problems during the CI process. However, by defining a Domain-Specific Language (DSL) in Go, it is possible to enforce that all APIs maintain a consistent uniformity.
Representative projects utilizing this technique include Kubernetes (which builds its own without a separate library), and it can also be used with projects such as go-restful and goa. The following is an example of goa usage:
1var _ = Service("user", func() {
2 Method("create", func() {
3 Payload(UserPayload)
4 Result(User)
5 HTTP(func() {
6 POST("/users")
7 Response(StatusOK)
8 })
9 })
10})
By writing compilable Go code as shown above, one gains the advantage of simultaneously completing both the implementation and the documentation definition for the POST /users API.
Advantages:
- Since everything originates from the code, it is easier to maintain API consistency across the entire project.
- By leveraging Go's strong typing system, one can obtain a more accurate and unambiguous specification compared to utilizing all features of OpenAPI3.
Disadvantages:
- One must learn the DSL defined by each framework, and it may be difficult to apply to existing code.
- Since one is forced to follow the rules proposed by the framework, freedom and flexibility may be reduced.
Conclusion
Each method possesses advantages and disadvantages, and it is crucial to select the most appropriate approach based on project requirements and team preferences. What is always most important is not which method is superior, but rather performing a value judgment to determine the most suitable solution for one's current situation, thereby maximizing development productivity to achieve early departures and satisfying work-life balance.
Although this article was written as of October 2024, the Go and OpenAPI ecosystems are continuously evolving. Therefore, considering the time elapsed since this writing, it is recommended to continuously follow up on the status of each library and project, as well as their changed advantages and disadvantages.
May your Go life be joyful! π