OpenAPI Generators

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/