Routing
Introduction to route definitions in Ply.
Configure Routing
Ply provides a router with static, parametric and wildcard routes, plus middleware chaining and route groups. Ply uses a tiered routing engine that separates static, parametric and wildcard routes into different tiers for fast resolution.
Ply precompiles handler chains, passes context with a zero-copy strategy and avoids unnecessary allocations. The design targets concurrency and minimal latency.
On routing-heavy workloads, internal benchmarks place Ply ahead of Gin and Fiber.
Basic example
app := ply.New()
app.Get("/hello", func(c *core.Context) {
c.Text(200, "Hello, World!")
})Route structure
Ply uses clear method-based route definitions:
app.Get(path, handler)app.Post(path, handler)app.Put(path, handler)app.Delete(path, handler)- And more...
Grouping Routes
Group routes under a common prefix:
group := app.Group("/api")
group.Get("/users", UserListHandler)
group.Post("/users", CreateUserHandler)Compiled routing
When the server starts, Ply transcribes the radix tree into a compiled-switch table (per-method decision trees + ordered wildcard prefixes). Static routes keep their O(1) map fast path; parametric and wildcard lookups run branch-predictor-friendly whole-segment compares with zero allocations.
The compiled table is exact by construction: a 200k-case differential
fuzz suite (internal/router/fuzz_test.go) asserts byte-identical
results against the radix tree on every change, and anything the table
cannot express falls back to the tree automatically.
Measured on AMD Ryzen 5 5600H (go test -bench, 0 allocs/op):
| Route kind | ns/op |
|---|---|
| Static | ~15 |
| Param | ~46 |
| Wildcard | ~38 |
Params are delivered through inline slots on the pooled request context (8 slots, no map on the hot path), which cut param lookup from ~66ns to ~46ns and wildcard from ~71ns to ~38ns in the same-session A/B. Paths that overflow the slots, or routes resolved by the tree fallback, use a map instead. Results are identical, and only the fast case is allocation-free.
Routes registered after startup mark the table dirty and it rebuilds lazily on the next lookup. No restart or manual step is needed.
Path parsing
fasthttp parses the request URI and normalizes the path (percent-decoding,
duplicate slashes, /./, /../) on the first ctx.Path() call, which
dominates handler CPU for simple routes. Ply checks the raw request
target first and uses it directly when normalization provably cannot
change it (no %, \, //, /./, /.. or control bytes, with the query
and fragment trimmed), falling back to ctx.Path() otherwise. Routing input
is byte-identical either way: a 50k-target differential test asserts the
fast path always matches the parser.
Measured on the same machine (go test -bench, 0 allocs/op):
ctx.Path() takes 105 ns, compared with 14 ns for the guarded fast path,
roughly 91 ns less CPU per request with no change in matching behavior.

Ply