Skip to content

Serve Interactive API Docs

GTB's pkg/openapi package serves an OpenAPI specification alongside an interactive Stoplight Elements docs site โ€” complete with a "try it" console โ€” from a single Register call. The Stoplight Elements UI is embedded in the framework, so your project ships only its generated spec. There is no per-project vendoring of the UI.

This guide takes you from a .proto file to a browsable, runnable docs site mounted alongside your REST API.


Prerequisites

You need an existing REST/gateway server (an *http.ServeMux exposing your grpc-gateway routes). If you're starting from gRPC, see Expose a gRPC Service as REST first โ€” the docs mount onto the same mux.


Step 1: Install the v3 Generator Plugin

grpc-gateway ships its own OpenAPI generator (protoc-gen-openapiv2), but it emits OpenAPI v2 (Swagger). Stoplight Elements wants OpenAPI v3, so we use a v3 generator instead โ€” kollalabs/protoc-gen-openapi, which understands the same google.api.http annotations as the gateway and emits OpenAPI 3.x.

Install it:

go install github.com/kollalabs/protoc-gen-openapi@latest

This puts the protoc-gen-openapi binary on your PATH, where buf can find it as a local plugin.


Step 2: Generate the v3 Spec with buf

Add a plugin entry to your buf.gen.yaml alongside the protoc-gen-go, protoc-gen-go-grpc and protoc-gen-grpc-gateway plugins you already run:

version: v2
plugins:
  - local: protoc-gen-go
    out: internal/gen
    opt: paths=source_relative
  - local: protoc-gen-go-grpc
    out: internal/gen
    opt: paths=source_relative
  - local: protoc-gen-grpc-gateway
    out: internal/gen
    opt: paths=source_relative
  # OpenAPI v3 (kollalabs/protoc-gen-openapi) โ€” understands google.api.http
  # annotations and emits OpenAPI 3.x. Written into the docs asset bundle so it
  # is embedded and served alongside the Stoplight UI.
  - local: protoc-gen-openapi
    out: internal/docs/assets
    opt:
      - title=Widget API
      - default_response=false

A few things worth calling out:

  • out: internal/docs/assets writes the spec straight into the package that embeds it (next step), so you never hand-copy the file.
  • opt: title=Widget API sets the info.title on the generated document. Make it match the title you give Stoplight in Step 4.
  • default_response=false keeps the generator from inventing a catch-all error response on every operation.

Run the generation:

buf generate

You should now have internal/docs/assets/openapi.yaml.


Step 3: Embed the Spec

The openapi.Register call takes the spec as a []byte. Embed the generated file into the binary with //go:embed so there's nothing to ship or locate at runtime:

// internal/docs/docs.go
package docs

import (
    _ "embed"
    "net/http"

    "gitlab.com/phpboyscout/go-tool-base/pkg/openapi"
)

//go:embed assets/openapi.yaml
var spec []byte

Embedding only your own spec keeps the project lean โ€” the Stoplight Elements assets (the JavaScript and CSS) ship inside go-tool-base, not your repository.


Step 4: Register the Docs Endpoints

Call openapi.Register with your mux and the embedded spec. It mounts two endpoints:

Method & Path Serves
GET /openapi.yaml The spec bytes (Content-Type application/yaml)
GET /docs/ The Stoplight Elements UI (the try-it console)
// Register mounts /openapi.yaml and the Stoplight docs site (/docs/) onto mux.
func Register(mux *http.ServeMux) error {
    return openapi.Register(mux, spec, openapi.WithTitle("Widget API"))
}

WithTitle sets the docs page title; the defaults (/openapi.yaml and /docs/) can be overridden with WithSpecPath and WithDocsPath if you need them elsewhere.


Step 5: Mount on the Same Server as Your API

This is the step that makes the "try it" console actually work, so don't skip it: register the docs onto the same *http.ServeMux that serves your REST/gateway routes.

mux := http.NewServeMux()

// Your existing REST/gateway handler, e.g.
// mux.Handle("/", gatewayMux)

if err := docs.Register(mux); err != nil {
    return err
}

When the spec and the docs site are served from the same origin as the API, Stoplight's "try it" console can call your endpoints directly โ€” no CORS configuration, no proxy, nothing. (Internally the Elements UI is configured with tryItCredentialsPolicy="same-origin", which is what that buys you.) Split the docs onto a separate server or port and the browser's same-origin policy will block the console's requests, and you'd be back to writing CORS rules to undo it.


Step 6: Test It

Start your server, then curl the spec endpoint:

curl -s http://localhost:8080/openapi.yaml | head
# openapi: 3.0.3
# info:
#   title: Widget API
#   ...

Then open the docs site in a browser:

http://localhost:8080/docs/

You should see the Stoplight Elements UI with your operations in the sidebar. Pick an endpoint, fill in the request, and hit Send โ€” the call goes to your live API on the same origin and the response comes back in the console.