From Tests to Docs: A Complete OASWrap Workflow
The three OASWrap libraries each solve one piece of the API documentation problem. Used together, they cover the full journey from spec generation to serving interactive docs — with no annotations, no third-party services, and no spec drift.
This post walks through a complete workflow: generate the spec with gswag during CI, embed it into your binary, and serve it with spec-ui.
The three libraries
| Library | Role |
|---|---|
gswag | Generates the OpenAPI spec as a side-effect of Ginkgo integration tests |
spec | Generates the OpenAPI spec from Go code at build time (no tests required) |
spec-ui | Serves the spec as interactive documentation via any Go HTTP handler |
gswag and spec are alternative paths to the same output — an OpenAPI YAML or JSON file. spec-ui consumes that file regardless of how it was produced.
Step 1 — Generate the spec
Option A: from integration tests with gswag
If you use Ginkgo, your spec comes for free from the tests you are already writing.
// suite_test.go
var _ = BeforeSuite(func() {
gswag.Init(&gswag.Config{
Title: "My API",
Version: "1.0.0",
OutputPath: "./docs/openapi.yaml",
})
testServer = httptest.NewServer(NewRouter())
gswag.SetTestServer(testServer)
})
var _ = AfterSuite(func() {
testServer.Close()
Expect(gswag.WriteSpec()).To(Succeed())
})
// users_test.go
var _ = Path("/users/{id}", func() {
Get("Get user by ID", func() {
Tag("users")
Parameter("id", PathParam, String)
Response(200, "user found", func() {
ResponseSchema(new(User))
SetParam("id", "1")
RunTest(func(resp *http.Response) {
Expect(resp).To(HaveStatus(http.StatusOK))
Expect(resp).To(MatchJSONSchema(&User{}))
})
})
})
})
Run go test ./... and ./docs/openapi.yaml is written. The spec only contains operations that were actually tested.
Option B: from Go code with spec
If you prefer build-time generation without running tests:
// cmd/generate/main.go
func main() {
r := spec.NewRouter(
option.WithTitle("My API"),
option.WithVersion("1.0.0"),
)
r.Get("/users/{id}",
option.Summary("Get user by ID"),
option.Tags("users"),
option.Request(new(GetUserRequest)),
option.Response(200, new(User)),
)
if err := r.WriteSchemaTo("./docs/openapi.yaml"); err != nil {
log.Fatal(err)
}
}
Run go run ./cmd/generate to regenerate the spec. This approach suits projects that don't use Ginkgo or need the spec available before tests run.
Step 2 — Embed the spec in your binary
Use Go's embed package so the spec ships inside the binary — no external file dependencies at runtime:
//go:embed docs/openapi.yaml
var specFS embed.FS
Step 3 — Serve with spec-ui
Wire the embedded spec to a UI provider. Here is Swagger UI:
import (
"embed"
"net/http"
specui "github.com/oaswrap/spec-ui"
"github.com/oaswrap/spec-ui/swaggerui"
)
//go:embed docs/openapi.yaml
var specFS embed.FS
func main() {
handler := specui.NewHandler(
specui.WithSpecEmbedFS("docs/openapi.yaml", specFS),
swaggerui.WithUI(),
)
mux := http.NewServeMux()
mux.Handle("/docs/", handler)
mux.Handle("/", apiHandler())
http.ListenAndServe(":8080", mux)
}
Switch to a different UI by changing the import — the rest of the code is identical:
import "github.com/oaswrap/spec-ui/scalar" // Scalar
import "github.com/oaswrap/spec-ui/redoc" // ReDoc
import "github.com/oaswrap/spec-ui/rapidoc" // RapiDoc
import "github.com/oaswrap/spec-ui/stoplight" // Stoplight Elements
Putting it together in CI
A typical GitHub Actions workflow for the gswag path:
jobs:
test-and-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Run tests and generate spec
run: go test ./...
# writes ./docs/openapi.yaml as a side-effect
- name: Build binary (spec embedded)
run: go build -o bin/api ./cmd/api
- name: Upload spec artifact
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: docs/openapi.yaml
For the spec path, replace the test step with go run ./cmd/generate.
The full picture
┌─────────────────────────────────────────┐
│ Source code │
└──────────┬───────────────┬──────────────┘
│ │
go test ./... go run ./cmd/generate
(gswag path) (spec path)
│ │
└───────┬───────┘
│
docs/openapi.yaml
│
┌───────▼───────┐
│ embed.FS │
└───────┬───────┘
│
┌───────▼───────┐
│ spec-ui │ GET /docs/
│ (any router) │
└───────────────┘
Each layer has a single responsibility. Swap the spec source, the UI provider, or the HTTP router independently — none of them are coupled to each other.