I’m a huge fan of the gRPC Protocol Buffer Recipe Workflow: Schema first to generate Client Libraries and Server Stubs (even easier when using goimpl
) - the only downside that i encountered so far is that there’s no first-class way to make use gRPC Services on the Frontend (i don’t consider grpc-gateway
to be first-class, generally i’m not keen on setting up a proxy for this kind of communication). Maybe i just need to accept that gRPC just wasn’t made with Frontends in mind. Nevertheless i’d love to use this Post to venture into the world of OpenAPI to venture into the world of OpenAPI to figure out if i can find a workflow that’s similar to gRPC generators. I’ll be making use of Go
for backends and TypeScript
for frontend-code.
Schema
The core idea of API First, sometimes referred to as Schema First, is that software teams start by defining an API contract and use it as the single source of truth for data models in their application logic.
Source: https://openapistack.co/docs/api-first/
Schema in OpenAPI can consist of YAML
or JSON
- depending on your preference, though i encountered more YAML than JSON on my research, so this might be your safe bet if you’d like to follow third party posts (like this one) or tutorials.
In this first step i’m going to manually create a Schema and try to generate both stubs and client libraries from this schema. I’ll also add a hint on how efficient LLMs might be while generating a schema (to satisfy my lazy).
Writing a Schema
First i used ChatGPT with the following prompt:
can you help me write a openapi 3.1 compatible schema for a simple web service?
the service has two methods:
- "/echo", method is post, the body should contain a single param called "message" of type string and return a body of the exact same type
- "/gulp", method is post, the body should contain a single parameter called "message" and return nothing
which generated something useful (i guess), but i won’t parse it here since it’ll overflow the whole post. You will find all files and code and contents in this (TODO: ADD) GitHub Repository.
At first my parser threw an error with string
literals not formatted correctly. I told ChatGPT to fix this and then it worked.
Generating Code
After having a schema file ready i was about to generate code for both server and client side.
Go-Swagger
go-swagger
would’ve been the obvious choice if it wasn’t for it being discontinued AND only supporting OpenAPI 2.0, which automatically disuqalifies it. Running through the SDK List of the OpenAPI Tool list there’s only the three sponsors that apparently support Go
as a target. They all come with a price tag, which is rather unfortunate for Playground-projects.
Ogen
Then i found ogen
(https://github.com/ogen-go/ogen) which as per their README
support Statically typed client and server
I added a generate.go
file to the packae and the generator line //go:generate go run github.com/ogen-go/ogen/cmd/ogen@latest --target test --clean api/schema.yml
as per the documentation of ogen
and was greeted by some generated code. It also, as promised, generated server code with an interface that describing both endpoints:
// Handler handles operations described by OpenAPI v3 specification.
type Handler interface {
// EchoPost implements POST /echo operation.
//
// Echo the received message.
//
// POST /echo
EchoPost(ctx context.Context, req *EchoPostReq) (*EchoPostOK, error)
// GulpPost implements POST /gulp operation.
//
// Receive a message without returning anything.
//
// POST /gulp
GulpPost(ctx context.Context, req *GulpPostReq) error
}
This is convenient as i can now just use goimpl
(and re-use gompl
on changes) for in own Handler
. Generally i like this idea BUT not being able to use other muxers is actually not an option for me. Sadly.
oapi-codegen
The first thing i noticed was that oapi-codegen
supports different servers, like echo
, fiber
, gorilla/mux
and net/http
(so every major server framework), that was the main critique for ogen
, so let’s see what’s going on here.
I followed the installation instructions from the repository, but also added a config.yaml to the root path of my project:
# yaml-language-server: $schema=../../../../configuration-schema.json
package: oapicodegen
output: playground.gen.go
generate:
models: true
echo-server: true
By the way, Speakeasy (one of the Premium Paid Codegen-Providers) is a direct Sponsor of oapi-codegen
, which makes me feel that the result of paying them would be similar to what we generated ourselves. Now we should only need to run go generate ./...
again. We now have a playground.gen.go
file as per our config. This is much smaller and (imo) nicer than ogen
. No batteries included, we’ll try to create a simple echo
server that servers our newly created API. And it does, actually, putting the api in the \api
folder, renaming the package (to make it easier) and we’ve got a very simple echo server running:
type MyHandler struct{}
// Echo the received message
// (POST /echo)
func (my *MyHandler) PostEcho(ctx echo.Context) error {
var body api.PostEchoJSONRequestBody
if err := ctx.Bind(&body); err != nil {
return err
}
resp := &api.PostEchoJSONBody{
Message: body.Message,
}
return ctx.JSON(http.StatusOK, resp)
}
func main() {
e := echo.New()
api.RegisterHandlers(e, &MyHandler{})
e.Start(":8080")
}
Test it with cURL: curl -X POST http://localhost:8080/echo -d "{\"message\": \"hi\"}" -H "Content-Type: application/json"
it works, just like i wanted. Perfect. I will stop my search for now.
Dear Reader, if you know of a better or another nice way to deal with this, feel free to hit me up.
Client API
For the Client it seems that openapi-generator is the go-to choice, despite it being written in java i will try it. Well, there are a plethora of invocation methods (brew, docker), so that’s a plus. Now i want to use the existing schema to generate a fetch
-based Typescript Client Library. openapi-generator generate -i api/schema.yml -g typescript-fetch -o ts/
seemed pretty easy?! Let’s figure out how easy it is to include it in a fresh vue.js project.
This was a rather rough start for me, but i got it working: To change the base URL of your API, you need to overwrite the DefaultConfig
provided by openapi-generator:
const apiConfig = new Configuration({
basePath: 'http://localhost:8088', // Custom base URL for your API
});
Then you need to use one of the generated models for your payload, the rest is self-explanatory:
defaultapi.echoPost({echoPostRequest: {message: foo.value}})
.then((response) => {
txt.value = response["message"]
})
.catch((error) => {
console.error('API error:', error);
});
Oh, one more little kink that got me on the wrong foot is that the API Library apparently does not work when the CORS preflight is not available, so you might just enable CORS on your backend, which with echo is as easy as:
e.Use(middleware.CORS())
Schema pt. 2
I still don’t like the verbosity of the schema.yml
file, maybe there’s something that might make it even easier to create them
Conclusion
This combination seems simple enough for my needs, also it works almost as easy as i wished it to be, now i might need to figure out if an ApiWrapper
would be a good idea, or a factory. But that’s a To-Do for future me.
Links
The official Schema Description from OpenAPI: https://swagger.io/specification/ A pretty exhaustive list of OpenAPI Tooling: https://openapi.tools/