Guide
Routing
Register routes, capture path parameters and wildcards, understand matching precedence, and handle 404 and 405 responses.
A route pairs an HTTP method and a path pattern with a handler chain. Zinc uses the brace syntax from Go 1.22’s net/http, so patterns look the same whether a Zinc handler or a standard handler serves them.
app.Get("/users", listUsers)app.Post("/users", createUser)app.Get("/users/{id}", showUser)app.Get("/assets/{path...}", serveAsset)Methods
Section titled “Methods”Each common method has a helper: Get, Post, Put, Patch, Delete, Head, Options, Connect, and Trace.
app.Put("/users/{id}", updateUser)app.Delete("/users/{id}", deleteUser)For anything else, name the methods yourself:
app.Add("PURGE", "/cache/{key}", purgeCache) // one custom methodapp.Match([]string{zinc.MethodGet, zinc.MethodHead}, "/ping", ping) // a chosen setapp.All("/echo", echo) // every standard methodPath parameters
Section titled “Path parameters”A {name} segment captures exactly one non-empty path segment. Read it with c.Param.
app.Get("/teams/{team}/users/{user}", func(c *zinc.Context) error { return c.JSON(zinc.Map{ "team": c.Param("team"), "user": c.Param("user"), })})Parameters are strings. Convert and validate them in the handler, or bind them into a typed struct with path:"team" tags. Zinc deliberately has no regex constraints in patterns.
Wildcards
Section titled “Wildcards”A final {name...} segment captures the rest of the path, including slashes. It can also be empty.
app.Get("/files/{path...}", func(c *zinc.Context) error { return c.String(c.Param("path")) // "/files/css/app.css" gives "css/app.css"})Matching rules
Section titled “Matching rules”When more than one route could match a request, Zinc picks the most specific one, segment by segment:
- Static segments win over parameters:
/users/mebeats/users/{id}. - Parameters win over wildcards:
/files/{name}beats/files/{path...}.
A few more rules round out the behavior:
- Case. Literal segments ignore case by default, so
/Usersmatches/users. Captured values keep their original case. SetCaseSensitiveto change this. - Trailing slashes.
/usersand/users/are the same route by default. SetStrictRoutingto treat them as different. - Encoding. Matching uses
Request.URL.Path, the decoded path. - Conflicts. Parameter names do not make routes distinct:
/users/{id}and/users/{name}conflict for the same method.
Pattern grammar
Section titled “Pattern grammar”pattern = "/" [ segment { "/" segment } ]segment = literal | parameter | catch-allparameter = "{" identifier "}"catch-all = "{" identifier "...}" ; final segment onlyAn identifier contains letters, digits, and underscores, and cannot start with a digit. A parameter must fill a whole segment, and each name can appear only once per pattern.
Some net/http pattern features are intentionally not supported: method or host prefixes inside ordinary route paths, the {$} end marker, and ServeMux’s overlap resolution. HandleHTTP("GET /users/{id}", h) accepts a method prefix because the method is split off before matching.
Invalid patterns fail at startup
Section titled “Invalid patterns fail at startup”Bad patterns panic when they are registered, so a mistake stops the program at boot instead of hiding until the first request.
/users/:id use /users/{id}/files/*path use /files/{path...}/users/prefix-{id} a parameter must fill a whole segment/files/{path...}/meta a catch-all must be last/users/{id}/{id} parameter names must be uniqueWhen patterns come from configuration or plugins, use TryHandle to get an error instead of a panic.
Groups
Section titled “Groups”A group shares a path prefix and middleware across related routes.
api := app.Group("/api", requireAPIKey)v1 := api.Group("/v1")
v1.Get("/users/{id}", showUser) // GET /api/v1/users/{id}, runs requireAPIKey firstv1.Post("/users", createUser)Route does the same with a nested block, which some teams find easier to scan:
app.Route("/api", func(api *zinc.Group) { api.Route("/v1", func(v1 *zinc.Group) { v1.Get("/users/{id}", showUser) v1.Post("/users", createUser) })}, requireAPIKey)Groups and Middleware covers ordering and scoping in detail.
Not found and method not allowed
Section titled “Not found and method not allowed”Zinc answers routing misses with the right status:
| Request | Default response |
|---|---|
| No route matches the path | 404 Not Found |
| The path exists, but not for this method | 405 Method Not Allowed with an Allow header |
OPTIONS for a known path |
204 No Content with an Allow header |
HEAD for a path with a GET route |
The GET handler runs, without a body |
The last three come from the HandleMethodNotAllowed, AutoOptions, and AutoHead settings, all on by default in zinc.DefaultConfig.
Replace the responses app-wide:
app.NotFound(func(c *zinc.Context) error { return c.Status(zinc.StatusNotFound).JSON(zinc.Map{"error": "not found"})})
app.MethodNotAllowed(func(c *zinc.Context) error { return c.Status(zinc.StatusMethodNotAllowed).JSON(zinc.Map{"error": "method not allowed"})})Or only below a prefix, for example to keep API misses in JSON while the rest of the site serves HTML:
app.RouteNotFound("/api/{tail...}", func(c *zinc.Context) error { return c.Status(zinc.StatusNotFound).JSON(zinc.Map{"error": "unknown api route"})})Named routes and URLs
Section titled “Named routes and URLs”Give a route a name to build its URL elsewhere without hard-coding paths.
app.Handle(zinc.RouteSpec{ Name: "users.show", Method: zinc.MethodGet, Path: "/users/{id}", Handler: showUser,})
url, err := app.URL("users.show", "42") // "/users/42"Routes from configuration
Section titled “Routes from configuration”Handle panics on an invalid spec, like every source-defined route. For patterns you do not control, TryHandle returns the error instead:
if err := app.TryHandle(zinc.RouteSpec{ Method: zinc.MethodGet, Path: patternFromConfig, Handler: showUser,}); err != nil { return fmt.Errorf("register route: %w", err)}Standard library handlers
Section titled “Standard library handlers”Any http.Handler can serve a route, and it reads parameters with r.PathValue:
app.HandleHTTP("GET /metrics", promhttp.Handler())
app.HandleHTTP("GET /users/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, r.PathValue("id"))}))
app.Mount("/legacy", legacyMux) // owns everything below /legacySee Zinc and net/http for the details.
Inspecting routes
Section titled “Inspecting routes”Route metadata stays available for tests, debug pages, and tooling.
all := app.Routes() // every route, in registration orderusers := app.RoutesByPrefix("/users") // one subtreeroute, ok := app.FindRoute(zinc.MethodGet, "/users/42") // what would serve this requestNext steps
Section titled “Next steps”- Groups and Middleware for scoping behavior to route families.
- Request Data for everything you can read from a request.
- Application API for the complete method list.
Named URLs reject empty or slash-containing values for single-segment parameters. Routing uses decoded URL.Path, so an encoded slash cannot represent a single segment. Use a catch-all parameter for multiple segments; generated catch-all values preserve / separators while escaping query, fragment, and percent characters.
Mounted handlers receive a cloned request with consistent URL.Path, URL.RawPath, and RequestURI. The original request remains available to outer middleware. Case-insensitive static routes retain precedence over parameter routes, including Unicode case folds; captured values preserve their original spelling.