LogoPly
Routing

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 kindns/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.