Routing

High-performance radix-tree routing with SoA hash table acceleration, root absorption, named parameters, wildcards, route groups, and pre-computed middleware chains. Static lookups in 7–12 ns, parametric matches in 19–36 ns, zero allocations.

Basic Routes

Register handlers by HTTP method. Each handler receives a *Request and *Response:

go
s := core.New()

s.Router.GET("/hello", func(req *core.Request, resp *core.Response) {
    resp.String("Hello, World!")
})

s.Router.POST("/users", func(req *core.Request, resp *core.Response) {
    resp.Status(201).String("Created")
})

The handler signature is always:

go
type HandlerFunc func(req *Request, resp *Response)

HTTP Methods

ALOS supports all 9 standard HTTP methods plus a catch-all:

Method Function Description
GET Router.GET(path, handler) Read resources
POST Router.POST(path, handler) Create resources
PUT Router.PUT(path, handler) Replace resources
DELETE Router.DELETE(path, handler) Delete resources
PATCH Router.PATCH(path, handler) Partial updates
HEAD Router.HEAD(path, handler) Headers only
OPTIONS Router.OPTIONS(path, handler) CORS preflight
CONNECT Router.Handle("CONNECT", ...) Tunnel
TRACE Router.Handle("TRACE", ...) Diagnostic
ANY Router.ANY(path, handler) All methods
go
s.Router.ANY("/health", func(req *core.Request, resp *core.Response) {
    resp.JSON([]byte(`{"status":"ok"}`))
})

s.Router.Handle("CONNECT", "/proxy", connectHandler)

Path Parameters

Use :name to capture named segments from the URL. Access them with req.ParamValue():

go
s.Router.GET("/users/:id", func(req *core.Request, resp *core.Response) {
    id := req.ParamValue("id")
    resp.String("User: " + id)
})

s.Router.GET("/posts/:postId/comments/:commentId", func(req *core.Request, resp *core.Response) {
    postId := req.ParamValue("postId")
    commentId := req.ParamValue("commentId")
    resp.String(postId + "/" + commentId)
})
Pattern URL Params
/users/:id /users/42 id = "42"
/posts/:postId/comments/:commentId /posts/5/comments/99 postId = "5", commentId = "99"

Up to 8 parameters per route are supported with zero-allocation lookup. Parameters are stored in a fixed-size array on the request.

Wildcard Routes

Use *name to capture everything after a prefix. This is useful for file serving or catch-all routes:

go
s.Router.GET("/files/*filepath", func(req *core.Request, resp *core.Response) {
    path := req.ParamValue("filepath")
    resp.SendFile("./static/" + path)
})

s.Router.GET("/*any", func(req *core.Request, resp *core.Response) {
    resp.Status(404).String("Not found")
})

Runtime note: wildcard matching works on the native Linux io_uring runtime, but this SendFile example uses the streamed-response compatibility path, which is not yet available on the native worker backend.

Pattern URL Params
/files/*filepath /files/css/style.css filepath = "css/style.css"
/*any /some/deep/path any = "some/deep/path"

A wildcard must be the last segment in the pattern. It captures everything after the prefix including slashes.

Route Groups

Group routes under a common prefix with shared middleware. Groups can be nested:

go
api := s.Router.Group("/api", core.Logger())

api.GET("/users", listUsers)
api.POST("/users", createUser)
api.GET("/users/:id", getUser)

admin := api.Group("/admin", authMiddleware)
admin.GET("/stats", getStats)
admin.DELETE("/users/:id", deleteUser)

Middleware applied to a group is cumulative — nested groups inherit parent middleware plus their own.

go
s.Router.Use(core.Recovery())
s.Router.Use(core.Logger())
s.Router.Use(s.CORS.Middleware())

protected := s.Router.Group("/dashboard", authMiddleware)

Groups support Use() to add middleware after creation, and ANY() for catch-all method registration:

go
api := s.Router.Group("/api")
api.Use(core.RealIP(), core.RequestID())

api.ANY("/health", healthCheck)

api.GET("/users", listUsers)
api.POST("/users", createUser)
RouteGroup Method Description
Group(prefix, middleware...) Create a nested sub-group
Use(middleware...) Add middleware to the group
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS Register handler for specific method
ANY(path, handler) Register handler for all methods

Building the Router

After registering all routes and middleware, call Build() to pre-compute the middleware chains:

go
s.Router.GET("/", homeHandler)
s.Router.GET("/about", aboutHandler)

s.Router.Build()

log.Fatal(s.ListenAndServeTLS())

Why Build()? Build pre-computes middleware chains, absorbs the empty root node into its first child, and constructs a struct-of-arrays hash table for O(1) static route lookups. This means zero overhead per-request — routing resolves in 7–12 ns for statics, 19–36 ns for parametric paths, with zero allocations.

404 & 405 Handlers

Customize the response for unmatched routes or disallowed methods:

go
s.Router.NotFound(func(req *core.Request, resp *core.Response) {
    resp.Status(404).JSON([]byte(`{"error":"route not found"}`))
})

s.Router.MethodNotAllowed(func(req *core.Request, resp *core.Response) {
    resp.Status(405).JSON([]byte(`{"error":"method not allowed"}`))
})

Route Lookup & Matching

Programmatically check if a route exists or look up its handler:

go
exists := s.Router.Match("GET", "/users/42")

handler := s.Router.Lookup("GET", "/users/42", req)
if handler != nil {
    handler(req, resp)
}

Global Middleware

Register middleware that runs on every route using Use(). Accepts variadic middleware:

go
s.Router.Use(core.Recovery(), core.Logger(), core.SecurityHeaders())

Middleware executes in registration order. Each wraps the next:

go
s.Router.Use(core.Recovery())
s.Router.Use(core.Logger())
s.Router.Use(core.Compress())

Groups inherit global middleware and can add their own:

go
s.Router.Use(core.Recovery())

api := s.Router.Group("/api", core.Logger())

api.Use(core.RealIP())

api.GET("/data", func(req *core.Request, resp *core.Response) {
    resp.String("Recovery + Logger + RealIP + handler")
})

Signature: func (r *Router) Use(middleware ...MiddlewareFunc)
MiddlewareFunc is func(HandlerFunc) HandlerFunc

Chain Helper

Apply middleware to a single handler without registering it globally. Useful for per-route middleware:

go
s.Router.GET("/admin/dashboard", core.Chain(
    dashboardHandler,
    authMiddleware,
    core.Timeout(5 * time.Second),
))

Mix global and per-route middleware:

go
s.Router.Use(core.Recovery(), core.Logger())

s.Router.GET("/public", publicHandler)

s.Router.POST("/upload", core.Chain(
    uploadHandler,
    core.BodyLimit(10 * 1024 * 1024),
    core.Timeout(30 * time.Second),
))

Signature: func Chain(handler HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc

Full Example

A complete routing setup with groups, middleware, custom error handlers, and route lookup:

go
s := core.New()

s.Router.Use(core.Recovery(), core.Logger(), core.Compress())

s.Router.GET("/", func(req *core.Request, resp *core.Response) {
    resp.String("Welcome")
})

s.Router.GET("/health", func(req *core.Request, resp *core.Response) {
    resp.JSON([]byte(`{"status":"ok"}`))
})

api := s.Router.Group("/api/v1", core.RealIP())

api.GET("/users", listUsers)
api.GET("/users/:id", getUser)
api.POST("/users", core.Chain(createUser, core.BodyLimit(1024*1024)))
api.DELETE("/users/:id", deleteUser)

admin := api.Group("/admin", authMiddleware)
admin.GET("/stats", statsHandler)
admin.POST("/purge", purgeHandler)

s.Router.GET("/static/*filepath", func(req *core.Request, resp *core.Response) {
    resp.SendFile("./public/" + req.ParamValue("filepath"))
})

s.Router.NotFound(func(req *core.Request, resp *core.Response) {
    resp.Status(404).JSON([]byte(`{"error":"not found"}`))
})

s.Router.MethodNotAllowed(func(req *core.Request, resp *core.Response) {
    resp.Status(405).JSON([]byte(`{"error":"method not allowed"}`))
})

s.Router.Build()

log.Fatal(s.ListenAndServeTLS())

Runtime note: the router, groups, and middleware chain all run on the native Linux io_uring runtime. The /static/*filepath example still uses SendFile, which remains a compatibility-only path for now.

ESC