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

jetwarp v1.0.0 is a portable Go HTTP routing + middleware + registration-error API. Portability is enforced by a shared conformance suite (tests/suite), not “best effort”.


Philosophy & Design Decisions

Jetwarp was built around a small set of convictions about what a routing library should and should not do. Understanding these helps explain why the API is shaped the way it is.


The application should not know what router it is running on

This is the central premise. Application code — handlers, middleware, route registration — imports only adapter.Adapter. The concrete driver (chi, gin, echo, fiber, or stdlib) is named in exactly one place: the call to adXXX.New() (or adapter/xxx.New()) in your main.go.

The consequence is that the same registerRoutes(r adapter.Adapter) function compiles and runs against every shipped adapter without a single change. This is not just a nice property to have — it is enforced by the conformance suite, which runs an identical test battery against all adapters.


Normalize at registration time (and mirror it at serve time)

Path pattern normalization and validation happen in a single place — the routingpath package — before the driver sees anything:

  • Leading slashes are enforced (patterns become absolute paths)
  • Trailing slashes are removed (except for /)
  • Internal double slashes are preserved intentionally to avoid surprising callers
  • Malformed {name} patterns are rejected (empty names, unbalanced braces, multiple brace pairs per segment, etc.)

This happens at registration time, and jetwarp mirrors the same “trailing slash normalization” at serve time in Router.ServeHTTP, so /docs/ reaches the same handler as /docs even if the underlying router framework treats trailing slashes differently.

One nuance worth calling out: routingpath.ValidatePattern is intentionally conservative. It does not try to normalize or reject internal // sequences. Instead, it focuses on the canonical pattern rules relevant to portable routing semantics.


Never panic. Accumulate errors instead.

Every registration method — Handle, HandleFunc, Group, With, Use — is designed to never panic.

Nil handlers, empty methods, invalid patterns, and unsupported middleware/features are recorded as errors and returned through r.Err(). Drivers are also protected: router registration uses a safeDriverHandle wrapper that catches panics from Driver.Handle(...) and converts them into ordinary errors.

The consequence is that a misconfigured router does not kill the process at startup. It surfaces problems through Err(), which returns a multi-error (*adapter.ListError) containing every issue. Callers can then decide:

  • log and abort, or
  • serve a hard 503 until the configuration is fixed (for staging/tests) using adapter.RefuseOnErr(...).

This design also makes tests simpler: you never need recover() in a test just to verify a bad input was rejected.


Typed-nil safety is part of the contract

Go interfaces can hold a typed-nil pointer (e.g. var d drv.Drv = (*MyDriver)(nil)), which looks non-nil but will panic when methods are called.

Jetwarp treats this as a first-class sharp edge:

  • Drivers implement IsNil() bool safely.
  • adapter.New(d) treats d == nil || d.IsNil() as “nil driver” and records jetwarp.ErrNilDriver instead of storing a value that could panic later.

Capabilities are promises, not aspirations

Every driver declares a drv.Capability bitmask. These bits are not hints or best-effort flags — they are promises that the conformance suite actively verifies.

For example: a driver that claims CapParamSuffix is promising it can support patterns like /files/{id}.json with the canonical meaning preserved. If a driver cannot provide correct, portable semantics, it should not claim the capability — and attempted registrations that depend on it should fail as ordinary configuration errors (not panics).

The practical consequence is that the capability matrix stays honest. If gin does not claim CapParamSuffix, it is because suffix/prefix params cannot be made portable on gin-style routing without changing semantics — not because nobody got around to implementing it.

Capabilities also make it possible to write adaptive setup code (or tooling) without surprises at runtime.


Middleware order is a contract, not an implementation detail

The execution order is fixed and guaranteed by the core adapter implementation, not by convention:

Global Use → Group → With → Per-route → Handler

Jetwarp assembles middleware by walking the scope chain from root to leaf and building the wrapper chain deterministically. No driver can change this ordering.

With() is worth highlighting separately: it returns a new scope and never mutates the receiver. This means you can freely mix public and protected endpoints within the same group without any risk of auth middleware leaking onto routes registered before the With() call. The immutability is structural, not “documented and hoped for”.

One more practical detail: jetwarp’s middleware surface is adapter.MW.

  • Shipped adapters support portable middleware (func(http.Handler) http.Handler) via adapter.HTTP / adapter.HTTPNamed.
  • Any non-portable middleware passed to Use/Group/With/Handle is currently rejected by the core router and recorded as jetwarp.ErrNativeMWUnsupported (documentation-only middleware is exempt; see adapter.DocCarrier).

The adapter.MW hook still matters: it leaves room for future or third‑party adapters to apply framework-specific middleware while preserving the same ordering contract. When that exists, portable and native middleware will remain layered; ordering is preserved within each category, but interleaving across portable vs native middleware in the same call is not promised.


The escape hatch is explicit and named

Jetwarp is a leaky abstraction by design. adapter.EngineProvider exists and is documented. You can retrieve the raw *gin.Engine, *echo.Echo, *fiber.App, chi.Router, or *http.ServeMux when you need it.

The difference from an unprincipled leak is that accessing the engine requires a conscious type assertion to a clearly named interface. There is no hidden state to stumble onto.


Separate modules — you pay only for what you import

Each adapter lives in its own module (adapter/chi, adapter/gin, etc.). The OpenAPI generator and validator also live in separate modules:

  • OpenAPI 3.2 generation: openapi/oas3
  • Dev-only spec validation: openapi/oas3/validate

Adding jetwarp to a project does not pull in chi, gin, echo, fiber, or OpenAPI dependencies unless you explicitly require the corresponding module. This keeps the dependency graph honest and makes go mod graph readable.

The monorepo is held together by a go.work file for local development. Published modules should not rely on replace directives — the workspace is a contributor convenience, invisible to downstream users.


Diagnostics are a machine-readable contract

The OpenAPI package (codeberg.org/iaconlabs/jetwarp/openapi/oas3, imported as v3) returns diagnostics from Build and Attach.

v3.DiagCode is part of the public API. Codes are stable strings suitable for programmatic checks in CI. Message text is explicitly not part of the contract and may change. Diagnostic ordering is also deterministic (build-time first, then attach-time, then UI provider diagnostics).

The same principle applies to errors: every error wraps jetwarp.ErrJetwarp, making errors.Is checks reliable regardless of wrapping depth.


Where to start