OpenAPI code generation with Go server and React app
When I created this blog last summer, I had several goals in mind: to acquire new skills, learn about fresh technologies, and have fun in the process. Its simplicity allowed me to rewrite the project multiple times using different programming languages and libraries: Rust with Axios, Java with Next.js, Java with Thymeleaf, and finally, Golang with React. While the first two options reduce the amount of code by handling both the service logic and the HTML rendering, they also take on too much responsibility in this way. Not that this is wrong by definition, but I prefer to separate these concerns.
However, this approach increases the workload. I had to build models on both sides, consider not only the controllers but also the client implementation, where changes on either side can break the integration. In this post, I want to explore a way to eliminate this overhead and explain how to generate code for a Go server and React client based on an OpenAPI specification. The idea is not limited to this stack, it can be easily applied elsewhere with suitable tools.
The full demo project is available on GitHub.
OpenAPI specification
It's not uncommon to generate the specification based on the actual code. For instance, with Spring Boot, you can add a single dependency that allows you to serve the swagger documentation created from annotated controllers, making the service the source of truth. But as soon as you have several components that need to come together at some point, it may be beneficial to introduce an independent contract to which all sides:
- Must adhere.
- Have to agree on how to change it.
- Can use for code generation.
And it doesn't really matter whether we're talking about one backend and one frontend developer, or many teams integrating with each other, or a single person like me. It saves time and ensures consistency. In case of a change, a single command can bring our code up to date!
Let's take a look at the specification I'm going to use for this project. It allows the user to add items to the news feed of some imaginary news network agency as well as retrieve existing items.
openapi: 3.0.0
info:
title: News Network API
version: 1.0.0
paths:
/news:
get:
description: Get the latest news feed
responses:
200:
description: List of latest news
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FeedItem"
500:
description: Internal Server Error
post:
description: Create a new item
requestBody:
content:
application/json:
schema:
type: object
properties:
title:
type: string
body:
type: string
breaking:
type: boolean
required:
- title
- body
- breaking
responses:
201:
description: Item created
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FeedItem"
400:
description: Bad request
500:
description: Internal Server Error
components:
schemas:
FeedItem:
type: object
properties:
id:
type: string
title:
type: string
body:
type: string
breaking:
type: boolean
required:
- id
- title
- body
- breaking
Same version visualized with Swagger Editor:
Server
To generate request handlers and models for the backend server, I'm going to use oapi-codegen. It's a popular tool that is actively developed and maintained. Moreover, it's built specifically for the Go ecosystem, meaning that the code it creates is compatible with popular libraries and frameworks.
First, install oapi-codegen:
go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest
Then, assuming the following project tree:
.
├── openapi.yaml
├── client
└── server
├── api
├── go.mod
├── go.sum
└── main.go
We can generate the code from the root directory with:
oapi-codegen -package api -generate spec,types,gin -o server/api/server.gen.go openapi.yaml
Where:
-package api
specifies the Go package name.-generate spec,types,gin
specifies exactly what needs to be generated:- spec - embeds the OpenAPI spec into the generated code, needed for automatic request body validation.
- types - generates models for all types in the OpenAPI spec, e.g.
FeedItem
from the contract we've defined above. - gin - generates handlers compatible with the Gin framework.
-o server/api/server.gen.go
specifies the output file for the generated code.openapi.yaml
specifies the input file with our specification.
Now we can create a server/api/handlers.go
file and put the actual controllers there. I'll use a simple struct as a dummy database to store feed items:
type Feed struct {
Items []FeedItem
}
func NewFeed() *Feed {
return &Feed{Items: make([]FeedItem, 0)}
}
And then implement two functions to handle incoming requests. GetNews
should return the array of existing items:
func (f *Feed) GetNews(c *gin.Context) {
c.JSON(http.StatusOK, f.Items)
}
While PostNews
should deserialize the request body and add it to the array. Since my design PostNewsJSONBody
doesn't have an id
property, we create a FeedItem
with the data from the request and a randomly generated id
.
func (f *Feed) PostNews(c *gin.Context) {
var request PostNewsJSONBody
if err := c.Bind(&request); err != nil {
c.Status(http.StatusBadRequest)
return
}
id := uuid.NewString()
f.Items = append(f.Items, FeedItem{
Id: id,
Title: request.Title,
Body: request.Body,
Breaking: request.Breaking,
})
c.Status(http.StatusCreated)
}
In this state, these controllers will never be called. We have to register them with Gin's router using a special function from server/api/server.gen.go
created for us by oapi-codegen:
engine := gin.Default()
api.RegisterHandlers(engine, api.NewFeed()) // this one
engine.Run()
It takes care of middleware and error handling. For instance, let's use it to handle the request body validation:
package main
import (
"github.com/gin-gonic/gin"
middleware "github.com/oapi-codegen/gin-middleware"
"github.com/parfentjev/blog-projects/codegen/server/api"
)
func main() {
swagger, err := api.GetSwagger()
if err != nil {
panic(err)
}
engine := gin.Default()
engine.Use(middleware.OapiRequestValidator(swagger)) // use the built-in validator
api.RegisterHandlers(engine, api.NewFeed())
engine.Run()
}
The GetSwagger
function is available because we requested the OpenAPI spec to be embedded with the code with the -generate spec
flag.
Add data
Let's launch the server:
cd server/
go run .
And make a request:
curl --request GET \
--url http://localhost:8080/news
[]
So far, the response is empty, but we can add a couple of items:
curl --request POST \
--url http://localhost:8080/news \
--header 'Content-Type: application/json' \
--data '{
"title": "Orange cats are adorable",
"body": "I swear to Zeus that they are!",
"breaking": false
}' && \
curl --request POST \
--url http://localhost:8080/news \
--header 'Content-Type: application/json' \
--data '{
"title": "Something has happened!",
"body": "We can confirm that something has just happened. Stay tuned...",
"breaking": true
}'
For our users to read them:
curl --request GET \
--url http://localhost:8080/news
[
{
"body": "I swear to Zeus that they are!",
"breaking": false,
"id": "e175d776-9293-4017-bcec-e874292eb5ab",
"title": "Orange cats are adorable"
},
{
"body": "We can confirm that something has just happened. Stay tuned...",
"breaking": true,
"id": "0120611f-2956-4c84-90c0-ceac2b133e66",
"title": "Something has happened!"
}
]
Superb! I also want to check that the request validation works. This JSON body lacks the breaking
property:
curl --request POST \
--url http://localhost:8080/news \
--header 'Content-Type: application/json' \
--data '{
"title": "Something has happened!",
"body": "We can confirm that something has just happened. Stay tuned..."
}'
{"error":"error in openapi3filter.RequestError: request body has an error: doesn't match schema: Error at \"/breaking\": property \"breaking\" is missing"}
Great success, the server is ready!
Testing
There are more convenient tools than curl when it comes to API testing. Postman and Insomnia are probably some of the most widespread programs for the job. Both can import an OpenAPI spec, so having one is useful for QA engineers too.
Importing our openapi.yaml
file:
The resulting requests with a dummy body:
Client
For the client code, we'll use another tool:
cd client
npm i @openapitools/openapi-generator-cli
npx openapi-generator-cli generate -i ../openapi.yaml \
-g typescript-fetch \
-o src/api
Where:
-i ../openapi.yaml
specifies the input file with our specification.-g typescript-fetch
specifies which generator to use,typescript-fetch
is targeted for TypeScript projects and makes HTTP requests with the Fetch API.-o src/api
specifies the output directory for the generated code.
As soon as the command is executed, we can start making requests to the API server with our web application. Here's an example for a React component:
const Feed = () => {
// declare a list of items
const [items, setItems] = useState<FeedItem[]>()
// declare an error message
const [error, setError] = useState<string>()
// when the component is rendered
useEffect(() => {
// create the API client
new DefaultApi(new Configuration({ basePath: 'http://localhost:8080' }))
// make a GET /news request
.newsGet()
// on success, add items to the list
.then((data) => setItems(data))
// on error, assign the error message
.catch((error) => setError(error.toString()))
}, [])
return (
<>
<div className="feed">
<h1>News Network Agency</h1>
{/* display error or render items, if any */}
{(error && <p>{error}</p>) ||
items?.map((i) => <Item key={i.id} item={i} />)}
</div>
<footer>© 2024 NNA</footer>
</>
)
}
Here's how it looks, with a breaking feed item highlighted in red!
Conclusion
This post summarizes some knowledge I've gained while integrating code generation for this blog. I appreciate its efficiency! If I make changes to the contract, I can regenerate both the server and client code. Thanks to static analysis, it's quick to identify breaking changes and address them. Previously, I'd have to manually check whether changes on the server-side affected the web app.
In a team setting, this means that as soon as any changes to the contract are agreed upon, developers working on different parts of the project can start modifying and testing their code, without waiting on others. This is also beneficial for QA engineers, particularly for test automation, because code generation eliminates the manual maintenance of request and response models. However, be aware that this makes negative testing more challenging, as the generated models and executors are not designed to send incorrect values, e.g., a string instead of a number.