This episode dissects chi's routing system: method routing, path params with curly braces, wildcards for the rest of the path, and regex patterns for param validation. You will also learn to handle NotFound and MethodNotAllowed, and the matching order that determines which route gets selected.

This is the most central episode of the whole series: routing, params, and patterns. This is where you learn how a URL is translated into a handler — from declaring r.Get and r.Post, capturing dynamic values in the middle of the path, to handling routes that aren't found.
After this episode, you'll never write flat, repetitive routes again. Every URL can capture data, validate formats, and handle fallbacks cleanly.
chi provides a helper for every HTTP method:
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Put("/users/{id}", updateUser)
r.Delete("/users/{id}", deleteUser)r.Post("/users", createUser) only serves the POST method; a GET request to the same path falls into the MethodNotAllowed handler (covered below). This is clean REST without branching on method inside the handler.
For methods without a helper — like PATCH or OPTIONS — use r.Method:
r.Method(http.MethodPatch, "/users/{id}", patchUser)
r.Method(http.MethodOptions, "/users", corsPreflight)r.Method(http.MethodPatch, "/users/{id}", patchUser) takes the method name as the first string and the handler as the last argument.
Dynamic params are written with curly braces inside the pattern:
r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) {
id := chi.URLParam(req, "id")
w.Write([]byte("user " + id))
})chi.URLParam(req, "id") reads the value captured by the {id} pattern. When a request arrives at /users/42, the variable id holds "42".
Important to remember: in chi v5, params are required. The route /users/{id} will not match /users — for that, you have to register a separate /users route. This is a deliberate simplification compared to version 4, which had an optional param syntax.
Constrain the param format with a regex inside the curly braces:
r.Get("/users/{id:[0-9]+}", numericUser)
r.Get("/articles/{slug:[a-z0-9-]+}", articleBySlug)The {id:[0-9]+} pattern only matches digits. A request to /users/abc won't reach this handler — it falls to NotFound, or to another route that matches better.
A wildcard captures the whole remaining path, useful for files and dynamic prefixes:
r.Get("/files/{path:*}", func(w http.ResponseWriter, req *http.Request) {
path := chi.URLParam(req, "path")
w.Write([]byte("file: " + path))
}){path:*} matches /files/a/b/c.txt with the value a/b/c.txt. chi.URLParam(req, "path") returns that whole string, including the slashes inside it.
r.Get("/users/{id}", handlerUser)
r.Get("/users/{id:[0-9]+}", handlerUserNumeric)
r.Get("/files/{path:*}", handlerFile)
r.Post("/users", handlerCreateUser)Notice how chi chooses: a more specific regex beats an ordinary param. handlerUserNumeric will catch /users/123, while handlerUser catches /users/abc.
When no pattern matches, chi calls the NotFound handler. When the path matches but the method doesn't, chi calls MethodNotAllowed:
r.NotFound(func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "halaman tidak ditemukan", http.StatusNotFound)
})
r.MethodNotAllowed(func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "method tidak diizinkan", http.StatusMethodNotAllowed)
})r.NotFound(handler) and r.MethodNotAllowed(handler) replace the default messages with your own JSON response or custom page.
curl -i http://localhost:8080/users/42
curl -i http://localhost:8080/tidak-ada
curl -i -X DELETE http://localhost:8080/users/42curl -i -X DELETE http://localhost:8080/users/42 tests a method that isn't registered — you should see status 405 Method Not Allowed with your custom message.
Understanding the matching order saves you from mysterious bugs:
{path:*} is always the last resort for a segment.So the patterns r.Get("/users/new", ...) and r.Get("/users/{id}", ...) can coexist: a request to /users/new is routed to the literal handler, while /users/7 goes to the param handler.
Key takeaways:
r.Get, r.Post, r.Put, r.Delete; other methods via r.Method.{id} is read with chi.URLParam(req, "id").{id:[0-9]+} constrains the format; wildcard {path:*} captures the rest of the path.r.NotFound and r.MethodNotAllowed handle fallbacks.In the next episode 5 we'll organize routes at a larger scale: subrouters, groups, and mounting — separating resources with r.Route, sharing middleware with r.Group, and attaching sub-apps with r.Mount.