OpenAPI 3.2 + Scalar
jetwarp can generate an OpenAPI 3.2 document from your router’s registry snapshot and optionally mount a documentation UI.
This page focuses on the Scalar integration (see examples/openapi-scalar-students).
Why this design
Registry-first generation (no reflection, no AST parsing)
jetwarp’s portability story depends on having one “truth source” across all adapters/drivers. We already maintain that truth source for routing: the registry snapshot (attempted routes, resolved paths, scope metadata, registration errors).
OpenAPI generation builds from that snapshot, which means:
- the same app code produces the same OpenAPI output no matter which underlying router driver is used
- docs generation does not rely on framework-specific reflection hacks
- output can be made deterministic (great for golden tests and long-term stability)
Optional OpenAPI module (keeps core lightweight)
The OpenAPI implementation lives in a dedicated module:
codeberg.org/iaconlabs/jetwarp/openapi/oas3(OpenAPI 3.2)
Most applications can generate docs without pulling in additional dev tooling. Validation (pb33f) lives in a separate dev-only module (see below), so you don’t pay runtime dependency cost unless you opt in.
Why Scalar
Scalar is a clean, modern API reference UI and is extremely easy to mount:
- by default it loads a pinned
@scalar/api-referencescript from a CDN - it only needs your OpenAPI JSON URL
- jetwarp’s provider mounts a redirect entrypoint and a working
index.htmlpage
If you later want Swagger UI, jetwarp also provides a Swagger UI provider — the OpenAPI JSON endpoint stays the same.
The “attach” mental model
oas3.Attach(router, cfg) does two things:
- Builds an OpenAPI 3.2 document from
router.Registry()and emits deterministic JSON. - Mounts endpoints on the router:
- always:
GET cfg.JSONPath→ OpenAPI JSON (default:/openapi.json) - optionally: docs UI under
cfg.Docs.Pathwhencfg.Docs.Path != ""andcfg.Docs.Provider != nil(misconfigurations don’t panic; they emit diagnostics)
- always:
A key safety goal: Attach must not panic. Even if JSON generation fails, it still mounts a minimal fallback JSON endpoint.
Best practice: call
Attachafter you register routes (once), then checkr.Err()and fail fast if needed.
Non-trivial example: “students” API + Scalar docs
This is the core wiring pattern from examples/openapi-scalar-students/main.go.
Highlights:
- routes are grouped under
/api - public read endpoints
- protected write endpoints via
With(authMW) - OpenAPI JSON is always mounted
- Scalar docs are optional (toggle with a flag)
- diagnostics are logged without crashing the server
- optional “dump OpenAPI JSON to file” for CI/debugging
package main
import (
"log"
"net/http"
"os"
"time"
twadapter "codeberg.org/iaconlabs/jetwarp/adapter"
v3 "codeberg.org/iaconlabs/jetwarp/openapi/oas3"
"codeberg.org/iaconlabs/jetwarp/openapi/oas3/ui/scalar"
)
func main() {
r := newRouter("stdlib") // or chi/echo/gin; see the example for CLI flags
// ---- middleware (portable net/http) ----
const maxTime = 5
r.Use(
twadapter.HTTPNamed("request_id", RequestID()),
twadapter.HTTPNamed("recover", RecoverJSON()),
twadapter.HTTPNamed("cors", CORS()),
twadapter.HTTPNamed("timeout", Timeout(maxTime*time.Second)),
twadapter.HTTPNamed("logger", AccessLog()),
)
store := NewStore()
api := NewAPI(store, tokenFromEnv())
// ---- routes ----
apiR := r.Group("/api")
apiR.HandleFunc(http.MethodGet, "/healthz", api.Healthz)
students := apiR.Group("/students")
// Public reads
students.HandleFunc(http.MethodGet, "/", api.ListStudents)
students.HandleFunc(http.MethodGet, "/{id}", api.GetStudent)
students.HandleFunc(http.MethodGet, "/{id}/grades", api.ListGrades)
// Protected writes (Auth middleware only for mutating endpoints)
protected := students.With(twadapter.HTTPNamed("auth", api.Auth()))
protected.HandleFunc(http.MethodPost, "/", api.CreateStudent)
protected.HandleFunc(http.MethodPut, "/{id}", api.UpdateStudent)
protected.HandleFunc(http.MethodDelete, "/{id}", api.DeleteStudent)
protected.HandleFunc(http.MethodPost, "/{id}/grades", api.AddGrade)
protected.HandleFunc(http.MethodPut, "/{id}/grades/{gid}", api.UpdateGrade)
protected.HandleFunc(http.MethodDelete, "/{id}/grades/{gid}", api.DeleteGrade)
// ---- OpenAPI ----
rr, ok := any(r).(v3.RegistryRouter)
if ok {
cfg := v3.Config{
Title: "Students API",
Version: "0.1.0",
JSONPath: "", // empty => default /openapi.json
}
// Scalar docs UI enabled:
cfg.Docs = v3.DocsConfig{
Path: "/docs", // set to "" to disable docs
Provider: scalar.Provider{}, // CDN mode by default
SpecURL: "", // empty => computed default
}
attached, diags := v3.Attach(rr, cfg)
for _, d := range diags {
log.Printf("openapi diag: code=%s msg=%q path=%q method=%q seq=%d",
d.Code, d.Message, d.Path, d.Method, d.Seq,
)
}
log.Printf("OpenAPI JSON mounted at %s", attached.JSONPath)
log.Printf("Docs UI (Scalar) mounted at %s", cfg.Docs.Path)
} else {
log.Printf("openapi disabled: router does not expose registry snapshots (missing adapter.RegistryProvider)")
}
if err := r.Err(); err != nil {
log.Printf("router configuration errors:\n%v", err)
os.Exit(1)
}
_ = http.ListenAndServe(":8080", r)
}
What this gives you at runtime
Assuming defaults:
- OpenAPI JSON:
GET /openapi.json - Scalar docs:
GET /docs(redirects to/docs/index.html)
The Scalar UI loads a single script (CDN by default) and fetches your OpenAPI JSON URL.
Scalar provider behavior (what gets mounted)
scalar.Provider{} mounts:
GET {DocsPath}→ redirect to{DocsPath}/index.htmlGET {DocsPath}/index.html→ serves HTML
Notes / gotchas:
- jetwarp normalizes away trailing slashes in registration, so providers avoid relying on
"/docs/"being distinct from"/docs". - if you need to self-host the Scalar script (air-gapped environments), set
scalar.Provider{CDNURL: "https://.../scalar.js"}.
Validating your OpenAPI output (recommended)
jetwarp ships a dev-only validator module powered by pb33f:
codeberg.org/iaconlabs/jetwarp/openapi/oas3/validate
Option A: validate the live server via URL
From the repo root:
go run ./openapi/oas3/validate/cmd/jetwarp-openapi-validate -url "http://127.0.0.1:8080/openapi.json"
You can also use the alias flag name:
go run ./openapi/oas3/validate/cmd/jetwarp-openapi-validate -openapi-url "http://127.0.0.1:8080/openapi.json"
Option B: validate a JSON file (great for CI artifacts)
go run ./openapi/oas3/validate/cmd/jetwarp-openapi-validate -file ./openapi.json
To validate from stdin:
cat ./openapi.json | go run ./openapi/oas3/validate/cmd/jetwarp-openapi-validate -file -
Option C: validate in tests (library API)
If you want a “spec must validate” test, the validator exposes a small API:
ok, issues, err := validate.Document(jsonBytes)
if err != nil {
t.Fatalf("validator setup failed: %v", err)
}
if !ok {
t.Fatalf("spec invalid; issues: %+v", issues)
}
Troubleshooting checklist
-
Docs loads but spec fetch fails: confirm the UI is pointing at the correct
SpecURL. If you have reverse proxies or mount OpenAPI under a different prefix, setDocs.SpecURLexplicitly. -
You changed routes but docs didn’t update:
Attachsnapshots the registry at call time. CallAttachafter registration (once), not before. -
You’re seeing OpenAPI diagnostics: these are non-fatal by design. Log them (like the example) and decide if your app should fail fast in production.
See the full runnable example
examples/openapi-scalar-students/(router + API + Scalar docs + flags)