Skip to content

Why a companion module

transport-openapi could have been a package inside go/transport — it mounts on a transport server's mux and depends on transport's security-header middleware. It is a separate module for one concrete reason: the size of what it embeds.

The 2.4 MB embed

The interactive docs UI is Stoplight Elements, vendored as two files:

Asset Size
web-components.min.js ~2.08 MB
styles.min.css ~297 KB

Both are //go:embed-ed into the binary. That is the price of shipping a self-contained docs site — a project needs only its generated spec, never a per-project copy of the UI front-end. It is a price worth paying when you serve docs.

If this lived in go/transport core, that ~2.4 MB would be linked into every transport-server binary — including the many services that expose no public API docs at all. A background worker with a health endpoint would carry a 2.4 MB Stoplight blob for no reason.

Opt-in by module boundary

Making it a separate import moves the cost to exactly the tools that want it:

  • Import go/transport → you get the server stack, no docs UI, no embed.
  • Also import go/transport-openapi → you opt into the docs handler and its embed.

Go's linker only includes packages that are imported, so the boundary is the switch. This mirrors the transport-metrics companion and the general transport-<companion> pattern in the toolkit: the transport core stays lean, and heavier or more specialised concerns live in companions that a tool adds only when it needs them.

What it still shares

Being a separate module does not mean duplicating transport. transport-openapi depends on go/transport and go/transit — it reuses their security-header middleware and Middleware type rather than reimplementing them. It is a companion, not a fork: lean core, opt-in extras, shared contracts.

Why embed the UI at all, rather than load it from a CDN

Two <script> and <link> tags pointing at unpkg or jsDelivr would make the generated page a few hundred bytes and the module tiny. It would also make every docs page load depend on a third party.

Embedding buys three things that matter more than the binary size:

  • The docs work wherever the binary does. An air-gapped deployment, a private network with no egress, a laptop on a train — the page renders because the page, the assets and the spec all come from the process you started. A CDN reference turns your API docs into a blank page the moment the network says no.
  • No third-party origin in the browser's view of your API console. The try-it console holds credentials for your API. A script served from someone else's domain, versioned by them, executes in that page. Vendoring the assets means the supply chain is your go.sum rather than a URL that resolves to whatever is there today.
  • The version is pinned by the module, not by a URL. The page always gets the Elements build this module was tested against — currently v9.0.0. It cannot change under you because upstream published a new major.

The cost is the one this whole module exists to isolate: 2.4 MB in the binary, and upgrading Elements means a change and a release here rather than editing a URL. That trade is why the boundary is a module rather than a flag — you pay it only by importing.

Why the docs handler and the transport server are separate concerns

They are separate because they are owned by different things. transport-openapi decides what is served at the docs and spec paths; go/transport decides how the server that carries them is configured, supervised and shut down. Neither needs to know much about the other — the whole contract between them is a *http.ServeMux and one middleware type.

That is why Register takes a mux rather than a server, and why it mounts routes rather than starting anything. A docs handler that also owned a listener would be two decisions welded together, and you would not be able to take one without the other. As it stands you can mount the docs on a bare http.ServeMux in a test, on a transport server in production, and on somebody else's router by nesting a mux inside it.