How we reduced oapi-codegen's dependency overhead by ~84%

Featured image for sharing metadata for article

As I announced recently, oapi-codegen, the OpenAPI to Go code generator that I co-maintain, will soon release a v2 release to allow us to reduce the size of the library's dependencies by roughly ~84%.

This is a pretty good saving, and has resulted in the following changes:

Before (v1.13.0)After (proposed v2.0.0)
Vendored dependency size (MB)406
Direct dependencies156
Indirect dependencies9534

So how have we ended up being able to shave so much off the package?

Existing project structure

Before we go into that, let's briefly review what the package is and how it works.

oapi-codegen is first and foremost a code generator that takes an OpenAPI 3.x specification and converts it to Go code. This can be done for either a client to consume an external API, or can be used to generate one of several servers, as well as allowing users to override the generation by providing their own text/template files.

Users of the package interact with us in two ways:

  • Using the CLI to generate the relevant code for them, whether using a tools.go approach or just go installing the version required,
  • Using oapi-codegen as a library, depending on packages for runtime or middleware functionality, as explained below

The high-level package structure looks like this:

cmd/oapi-codegen
examples              Example code for various servers and use-cases, as a means to show how you can get started, as well as what a realistic usecase would look like
internal/test/        Integration / regression tests to cover bugs or features
pkg/
    chi-middleware    HTTP middleware for Chi web server, as well as anything implementing net/http compatible interfaces such as gorilla/mux
    codegen           Actual code generation functionality exposed by oapi-codegen, sometimes imported as a library by other projects
    ecdsafile         Utility for working with ECDSA public and private keys
    fiber-middleware  HTTP middleware for Fiber web server
    gin-middleware    HTTP middleware for Gin web server
    iris-middleware   HTTP middleware for Iris web server
    middleware        HTTP middleware for Echo web server
    runtime           Runtime-specific code, such as converting a URL-encoded form request to a struct, or performing `allOf`/`anyOf`/etc manipulation
    securityprovider  Perform common authentication schemes for use with the generated HTTP client
    testutil          Utilities for making it easier to test HTTP handlers with a fluent interface
    types             Types that may be required by the generated code, such as a UUID type
    util              Utilities for handling command-line flags, validating JSON media types and loading OpenAPI specs

This structure mostly has existed since the v1.0.0 release in 2019, and has expanded over the years as we've added support for more servers and functionality.

Discovering the impact

In July, we received an issue on the issue tracker: v1.13.0 introduces lots of transitive dependencies to client library, and really appreciate Paul doing so!

We'd not (yet?) seen this as an impact on our side, and so this gave us an early indication that something was up.

This was introduced by an internal tweak to reduce some duplication across the generated code, introducing pkg/runtime/strictmiddleware.go:

// via https://github.com/deepmap/oapi-codegen/blob/v1.13.0/pkg/runtime/strictmiddleware.go
package runtime

import (
	"context"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/labstack/echo/v4"
)

type StrictEchoHandlerFunc func(ctx echo.Context, request interface{}) (response interface{}, err error)

type StrictEchoMiddlewareFunc func(f StrictEchoHandlerFunc, operationID string) StrictEchoHandlerFunc

type StrictHttpHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (response interface{}, err error)

type StrictHttpMiddlewareFunc func(f StrictHttpHandlerFunc, operationID string) StrictHttpHandlerFunc

type StrictGinHandlerFunc func(ctx *gin.Context, request interface{}) (response interface{}, err error)

type StrictGinMiddlewareFunc func(f StrictGinHandlerFunc, operationID string) StrictGinHandlerFunc

Although this by itself was a reasonable change, it led to quite an impact, and we should've caught it at the time.

Go is able to prune the dependency graph to reduce the need to download dependencies that aren't actually in use, but this works at a package-level, not a file-level, so now any user depending on pkg/runtime would also need to pull down Gin and Echo, even if they weren't using them.

Over the next release's lifetime, we started looking at what we could do to reduce this, in a way that wouldn't (yet) require a breaking change.

Considering a multi-module Go module

We'd had a discussion around trying to make these changes in-place in the repo we had right now, without requiring creating any new repos, i.e. deepmap/oapi-codegen-runtime, so the first approach was to create a multi-module repository.

I'd had a go at trying to do this, but we ended up abandoning this approach. As part of trying to integrate the end result with an example project, as well as discussing with the lovely people on the Gopher Slack, we found that moving to a multi-module project would be more difficult than we thought, would complicate releases, and didn't quite gel with what we wanted.

We found that trying to keep everything in one repo wasn't quite working for us, albeit I'm glad we tried as I enjoyed playing around with it and learning a bit more about Go modules.

Instead, we took some of the commits from that branch and moved examples and internal/test to their own Go modules as part of Work to reduce transitive dependencies. Neither of these packages are expected to be externally consumable, so we decided to hide these in their own Go modules, which use replace directives, allowing us to still use them for ensuring that our code generation works, but without polluting the top-level module with the dependencies they require.

We can see the changes introduced by Work to reduce transitive dependencies in the v1.14.0 release below:

Before (55641e96)After (v1.14.0)
Vendored dependency size (MB)4947
Direct dependencies1614
Indirect dependencies8174

As we can see, this was a pretty minimal change, but starts to move the needle with our cleanup journey.

New project structure

The other key thing as part of did was migrate the pkg/types and pkg/runtime packages to a completely new package, github.com/oapi-codegen/runtime.

Right now, any changes to the runtime or middleware related functionality required a new release of oapi-codegen, which recently has taken anywhere up to 6 months (😅) which requires folks then pin to direct commits to get a fix for an issue they're seeing, which isn't ideal.

After the failed attempt to try and keep everything in a single repo, we considered it a bit further and realised that it would probably be best to decouple the code generation and other pieces in the oapi-codegen ecosystem.

This allowed the v1.14.0 release to test the waters with the first separate package, and with the v1.16.0 release today, we've migrated all packages to a multi-repo approach. I'm looking forward to having this small v2 jump behind us, and to continue to keep an eye on number of dependencies we introduce in the future.

Anything we can improve on? Give us a shout through replying to the blog post, or on the discussion around the upcoming v2 release.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #go.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.