jetwarp — New Developer Onboarding Guide
Welcome to the team. This document is your first-day map of the jetwarp codebase: what each layer does, how they talk to each other, and what rules we treat as compatibility contracts.
Read it top to bottom once, then keep it nearby as a reference.
1. The Big Picture
jetwarp is a portable Go HTTP routing + middleware + registration-error API.
The core promise is:
Application code registers routes and middleware against one stable API (
adapter.Adapter). You can swap the underlying router backend (stdlib mux, chi, gin, echo, fiber, …) without rewriting your application routing setup.
Portability is not “best effort” — it is enforced by the shared conformance suite under tests/suite.
The layers
┌──────────────────────────────────────────────────────────┐
│ Your application │
│ registerRoutes(r adapter.Adapter) │
│ r.Group("/v1", ...).HandleFunc("GET", "/users/{id}") │
└───────────────────────┬──────────────────────────────────┘
│ adapter.Adapter
┌───────────────────────▼──────────────────────────────────┐
│ adapter.Router (core implementation) │
│ • validates + normalizes patterns │
│ • composes middleware deterministically │
│ • records the registry snapshot │
│ • delegates registration + serving to a driver │
└───────────────────────┬──────────────────────────────────┘
│ drv.Drv
┌───────────────────────▼──────────────────────────────────┐
│ drivers/v1/* (router drivers) │
│ • wrap a concrete router (chi/gin/echo/fiber/stdlib) │
│ • translate canonical patterns to native syntax │
│ • expose params + scoping through a common contract │
└──────────────────────────────────────────────────────────┘
Your app should only depend on adapter.Adapter (and net/http). Drivers are internal plumbing — you touch them when writing a new driver or debugging a driver behavior.
2. Repository layout
jetwarp is intentionally multi-module. Each adapter and driver lives in its own go.mod so downstream users only download what they import.
Common directories you’ll touch:
adapter/— public API + core router implementation (adapter.Router).drv/— driver contract:drv.Drv, capabilities, kinds, method canonicalization.drivers/v1/*— concrete drivers (stdlib/chi/gin/echo/fiber).compat/routingpath/— public façade for canonical path normalization/validation. Used by drivers, adapters, and openapi when they need to import from a separate module (Go’sinternal/rules prevent submodules from importinginternal/directly).internal/routingpath/— actual implementation. Only importable within the root module.tests/suite/— conformance tests (the portability “spec”).tests/testkit/— deterministic mock drivers used by the suite.openapi/oas3/— optional OpenAPI 3.2 generator (separate module).openapi/oas3/validate/— optional dev-only validator wrapper (separate module).
For local development across modules, contributors commonly use a go.work workspace. The workspace is a convenience for contributors; consumers should never need it.
3. The driver contract (drv)
File: drv/driver.go
Every backend router is represented as a drv.Drv.
A few guiding principles:
- Drivers are capability-driven: they declare what they can do via a bitmask (
Caps()), and core/suite only relies on behavior when the capability is claimed. - Drivers must be typed-nil safe: a Go interface can hold a typed-nil pointer value that panics when methods are called. Drivers implement
IsNil()so core can defend itself.
Capabilities are promises
File: drv/capabilities.go
Capabilities are not hints — they are promises verified by the suite.
| Capability | What it means | Why it exists |
|---|---|---|
CapScope | Scope(prefix) returns a derived driver whose routes are reachable only under that prefix. | Lets adapters implement Group(prefix) portably. |
CapParams | Canonical {name} params are supported and values are retrievable. | Enables portable path-params across backends. |
CapParamSuffix | In-segment prefix/suffix patterns work (e.g. /x/{id}.json). | Some routers can’t represent this without semantic changes. |
CapAnyMethod | Handle("*", …) is supported (method-agnostic routes). | Enables a portable “any method” registration. |
CapNativeScopeMW | Reserved — v1.1+. Not functional in v1. Defined to mark the slot for a future native-middleware-per-scope mechanism. Do not implement in v1. | Future extension point; the interface contract will be defined in v1.1. |
Rule of thumb: if you’re deciding behavior, prefer Caps() over Kind().
Kind() is for diagnostics and human labeling; Caps() is for correctness.
4. Canonical patterns (routingpath)
jetwarp routes on the URL path only (query string is ignored by matching).
Normalization
routingpath.NormalizePattern does the minimal normalization that keeps behavior predictable:
- trims whitespace
- ensures a leading
/ - removes trailing
/(except/) - does not collapse internal
//sequences
That last bullet is intentional: collapsing internal slashes can surprise callers, so validation focuses on brace correctness rather than rewriting paths.
Validation
routingpath.ValidatePattern enforces jetwarp’s canonical param rules (what drivers must agree on):
- rejects empty param names (
{}) - rejects unbalanced braces in a segment
- rejects multiple brace pairs in a segment
- rejects invalid param names (containing
/,{,})
If you see a route registration error, it is usually produced by NormalizePattern + ValidatePattern before the driver is touched.
5. The adapter (adapter.Router) — what apps use
Key files:
adapter/adapter.go(interfaces)adapter/router_implementation.go(core implementation)
Scopes: Use, Group, With
jetwarp intentionally models routing configuration as a tree of immutable “scopes”:
Use(mw...)attaches middleware to the current scope.Group(prefix, mw...)returns a derived scope with a path prefix and extra middleware.With(mw...)returns a derived scope that adds middleware without changing the prefix.
With is explicitly non-mutating. That structural immutability is what prevents middleware “leaks” between sibling routes.
Deterministic middleware ordering
The contract is:
Use → Group → With → per-route → handler
Core assembles middleware by walking the scope chain (root → leaf) and wrapping in a deterministic order. Drivers do not get to reorder it.
Portable middleware vs native middleware
The public middleware surface is adapter.MW, which allows two categories:
- Portable net/http middleware (
func(http.Handler) http.Handler) wrapped viaadapter.HTTP(...)/adapter.HTTPNamed(...). - Native middleware (framework-specific), represented as arbitrary
adapter.MWvalues.
Important current reality: the built-in adapters shipped with jetwarp currently support portable middleware only.
- Portable middleware is always accepted.
- Any non-portable middleware passed to
Use/Group/With/Handleis recorded asjetwarp.ErrNativeMWUnsupportedand is not applied. - Documentation-only middleware (
adapter.DocCarrier) is exempt and is extracted before middleware splitting.
Native middleware support is deliberately “future/third-party ready” — if an adapter chooses to support it, the suite enforces separation and ordering contracts.
Param portability: (*http.Request).PathValue
jetwarp bridges driver params into the stdlib-style API:
- drivers expose params via
drv.Drv.Param(r, key) - core copies them onto the request using
req.SetPathValue(key, value)
That means handlers can read {id} via:
id := r.PathValue("id")
…and it works the same across all adapters.
“Never panic” registration
Bad registrations must not panic. jetwarp accumulates errors and keeps the router usable:
Handle/HandleFunc/Use/Group/Withvalidate and record errors- registration panics from underlying frameworks are caught and converted to ordinary errors
- all issues are surfaced through
r.Err()
The goal is to make setup failures observable and testable without requiring recover() in application code.
Typed-nil safety
adapter.New(d) must never store a typed-nil driver.
Core treats d == nil || d.IsNil() as a nil driver, records jetwarp.ErrNilDriver, and nils out the stored driver to prevent later panics.
6. The registry snapshot
Every core router implements adapter.RegistryProvider via:
Registry() adapter.RegistrySnapshot
The snapshot is a deep-copied view of:
- scope records (root/groups/with scopes)
- route records (attempted registrations)
- errors recorded at scope level and route level
- merged documentation metadata (from
DocCarriermiddleware)
This registry is the “truth source” for downstream tooling.
7. OpenAPI 3.2 (optional module)
OpenAPI support is in the optional module:
codeberg.org/iaconlabs/jetwarp/openapi/oas3
Generation is deterministic and based on the registry snapshot.
The main UX entrypoint is:
attached, diags := oas3.Attach(router, oas3.Config{ ... })
- JSON is mounted at
Config.JSONPath(default/openapi.json). - Docs UI is mounted only when
Config.Docs.Path != ""andConfig.Docs.Provider != nil. - Diagnostics (
[]oas3.Diagnostic) are non-fatal and ordered; codes are stable for CI.
Validation tooling lives in:
openapi/oas3/validate/cmd/jetwarp-openapi-validate
(The binary name follows the folder name; renaming it is a potential future cleanup.)
8. Conformance testing (tests/suite)
The suite is the portability spec.
suite.RunDriverruns the driver battery against adrv.Drvfactory.suite.RunAdapterruns adapter-level batteries against anadapter.Adapterfactory.
Tests are capability-driven: a driver that does not claim CapParamSuffix will have suffix tests skipped.
The mock drivers (tests/testkit)
testkit.MockDriver is a deterministic in-memory driver used to validate core semantics.
testkit.LimitedMockDriver wraps MockDriver and lets tests “turn off” capabilities to exercise skip paths and error behavior.
9. Errors and diagnostics
jetwarp uses a sentinel error to make errors.Is checks reliable:
jetwarp.ErrJetwarp
Concrete errors wrap that sentinel (e.g. jetwarp.ErrInvalidPattern, jetwarp.ErrNilDriver, jetwarp.ErrNativeMWUnsupported).
For OpenAPI, oas3.Diagnostic.Code is the stable machine-readable contract. Message text is intentionally not treated as stable.
10. Local development workflow
Recommended (contributors): go.work
A workspace makes it easy to run tests and tools across modules locally:
go work init
# add the modules you’re actively editing
go work use . ./adapter/chi ./drivers/v1/chi ./openapi/oas3
go work sync
go test ./...
Conservative (always works): module-by-module
# root
(go test ./...)
# adapters/drivers
(cd adapter/chi && go test ./...)
(cd drivers/v1/chi && go test ./...)
# openapi
(cd openapi/oas3 && go test ./...)
(cd openapi/oas3/validate && go test ./...)
11. Invariants (do not break these)
- No panics on bad registration. Convert to errors and keep the router usable.
- Deterministic middleware order:
Use → Group → With → per-route → handler. - Capabilities are promises. Only claim a cap if the driver passes the suite for it.
- Method-specific beats ANY (
"*") for the same path+method, independent of registration order. - Typed-nil driver safety: core must never store a typed-nil driver.
- Registry snapshots are immutable (deep-copied) and safe to read anytime.
Group("")andGroup("/")are no-ops (no error, no prefix change). Whitespace-only prefixes recordErrInvalidGroupPrefixand behave as no-ops.
If you change anything that touches one of these, you should expect to add or update suite contract tests.