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

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’s internal/ rules prevent submodules from importing internal/ 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.

CapabilityWhat it meansWhy it exists
CapScopeScope(prefix) returns a derived driver whose routes are reachable only under that prefix.Lets adapters implement Group(prefix) portably.
CapParamsCanonical {name} params are supported and values are retrievable.Enables portable path-params across backends.
CapParamSuffixIn-segment prefix/suffix patterns work (e.g. /x/{id}.json).Some routers can’t represent this without semantic changes.
CapAnyMethodHandle("*", …) is supported (method-agnostic routes).Enables a portable “any method” registration.
CapNativeScopeMWReserved — 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:

  1. Portable net/http middleware (func(http.Handler) http.Handler) wrapped via adapter.HTTP(...) / adapter.HTTPNamed(...).
  2. Native middleware (framework-specific), represented as arbitrary adapter.MW values.

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/Handle is recorded as jetwarp.ErrNativeMWUnsupported and 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/With validate 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 DocCarrier middleware)

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 != "" and Config.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.RunDriver runs the driver battery against a drv.Drv factory.
  • suite.RunAdapter runs adapter-level batteries against an adapter.Adapter factory.

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

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)

  1. No panics on bad registration. Convert to errors and keep the router usable.
  2. Deterministic middleware order: Use → Group → With → per-route → handler.
  3. Capabilities are promises. Only claim a cap if the driver passes the suite for it.
  4. Method-specific beats ANY ("*") for the same path+method, independent of registration order.
  5. Typed-nil driver safety: core must never store a typed-nil driver.
  6. Registry snapshots are immutable (deep-copied) and safe to read anytime.
  7. Group("") and Group("/") are no-ops (no error, no prefix change). Whitespace-only prefixes record ErrInvalidGroupPrefix and behave as no-ops.

If you change anything that touches one of these, you should expect to add or update suite contract tests.