Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-reference script from a CDN
  • it only needs your OpenAPI JSON URL
  • jetwarp’s provider mounts a redirect entrypoint and a working index.html page

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:

  1. Builds an OpenAPI 3.2 document from router.Registry() and emits deterministic JSON.
  2. Mounts endpoints on the router:
    • always: GET cfg.JSONPath → OpenAPI JSON (default: /openapi.json)
    • optionally: docs UI under cfg.Docs.Path when cfg.Docs.Path != "" and cfg.Docs.Provider != nil (misconfigurations don’t panic; they emit diagnostics)

A key safety goal: Attach must not panic. Even if JSON generation fails, it still mounts a minimal fallback JSON endpoint.

Best practice: call Attach after you register routes (once), then check r.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.html
  • GET {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"}.

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, set Docs.SpecURL explicitly.

  • You changed routes but docs didn’t update: Attach snapshots the registry at call time. Call Attach after 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)