-
Notifications
You must be signed in to change notification settings - Fork 0
Routes and Schemas
Two live in this module, at different levels of detail.
// rextension.Route — the minimum the framework needs to register something.
type Route interface {
Method() string
Path() string
}
// rextension/route.Route — what a real route is.
type Route interface {
Method() string
Path() string
Handler() HandlerFunc
}rextension/route is the canonical package for handlers and route construction.
Extensions should import it rather than github.com/kryovyx/rex/route.
import rxroute "github.com/kryovyx/rextension/route"
rt := rxroute.New("GET", "/healthz", func(ctx rxroute.Context) {
ctx.JSON(200, map[string]string{"status": "ok"})
})Path parameters are written {name} and read with ctx.Param("name").
type Context interface {
context.Context
ResponseWriter() http.ResponseWriter
Request() *http.Request
Resolver() di.Resolver // the request scope
Respond(status int, contentType string, body interface{}) error
Text(status int, v string) error
JSON(status int, v interface{}) error
OpenMetrics(status int, v interface{}) error
Param(name string) string
SetValue(key, value interface{})
GetValue(key interface{}) interface{}
}Resolver() is the request scope: anything registered Scoped is built at
most once per request and closed when the request ends. It is typed as
di.Resolver, so a handler needs only this module.
SetValue stores a value on the context without replacing the request's own
context — useful for passing data from middleware to handler when you do not
want to rebuild the *http.Request.
rt, ok := rxroute.GetMatchedRoute(req)The router calls SetMatchedRoute after resolving a request to a concrete
route, so middleware running in the same request can reach the route without
re-parsing the URL. This is what lets a middleware ask "does this route
declare X?" at request time when it could not decide at build time.
Prefer deciding at build time via UsePerRoute where you can. Use
the matched route when the decision genuinely depends on the request.
A route is an interface, so an extension declares what it needs as a further interface and type-asserts. This is how every cross-cutting concern in the ecosystem attaches to routes without any of them sharing a type.
type MyRoute struct {
rxroute.Route
}
func (r *MyRoute) RequiredSchemes() []string { return []string{"bearer"} }| Interface | Declared in | Read by |
|---|---|---|
BodySchemaProvider |
rextension |
validation, openapi |
BodyLimitedRoute |
rextension |
the router |
SecuredRouteAccessor |
rextension |
security, openapi, swagger |
DependencyGatedRoute |
rextension-health |
health |
RateLimitedRoute |
rextension-ratelimit |
ratelimit |
A route that does not implement one is simply passed through.
type BodySchema interface {
Kind() SchemaKind
Types() []interface{}
}
type BodySchemaProvider interface {
RequestBody() BodySchema
Responses() map[int]BodySchema
}Four constructors build one:
rextension.Scalar(CreateUserRequest{}) // a single type
rextension.OneOf(CardPayment{}, BankPayment{}) // exactly one must match
rextension.AnyOf(A{}, B{}) // one or more may match
rextension.AllOf(Base{}, Extra{}) // all, mergedDeclared on a route:
func (r *CreateUser) RequestBody() rextension.BodySchema {
return rextension.Scalar(CreateUserRequest{})
}
func (r *CreateUser) Responses() map[int]rextension.BodySchema {
return map[int]rextension.BodySchema{
201: rextension.Scalar(UserResponse{}),
422: rextension.Scalar(rextension.Problem{}),
}
}Both the validation middleware and the OpenAPI generator read routes through
this one interface. A route that does not implement it is passed through
without validation and documented without schemas. Returning nil from
Responses skips response validation entirely.
The schema contract lives here rather than in the validation extension so that the OpenAPI generator can read it through a named interface. It previously reached those methods with
reflect.MethodByName("RequestBody")and type-asserted the result to[]interface{}— unchecked, so an unexpected slice type panicked inside document generation. Sharing a type removes the need for the reflection.
type BodyLimitedRoute interface {
MaxBodyBytes() int64 // 0 → the router's limit; -1 → no limit
}func (r *UploadRoute) MaxBodyBytes() int64 { return 64 << 20 } // 64 MiB
func (r *IngestRoute) MaxBodyBytes() int64 { return -1 } // streamingThe value is read once, when the route table is built, not per request. The router's own default is 4 MiB; see Router Configuration.
rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem