This episode teaches how to organize routes at scale: subrouters with r.Route, groups sharing middleware with r.Group, and mounting sub-apps with r.Mount. You will also learn to separate router resources and apply middleware per subrouter selectively.

Real applications are never satisfied with a single router file. Episode 5 teaches the structure that keeps routes tidy as your application grows: subrouters, groups, and mounting. These three mechanisms are how chi manages complexity without introducing new handler types.
The patterns you'll master here become the skeleton of the whole project structure in episode 8. If you've ever been confused by middleware not running on some routes, the answer is almost always in this episode.
r.Route creates a child router that inherits the parent router's middleware and lives under a single prefix:
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Route("/api", func(api chi.Router) {
api.Get("/ping", func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("pong"))
})
api.Get("/health", healthHandler)
})
http.ListenAndServe(":8080", r)
}All routes inside r.Route("/api", ...) automatically live under /api. api.Get("/ping", handler) produces the endpoint /api/ping. The child router inherits middleware.Logger from the parent without having to register it again.
Resources can be broken down even further with nested routes:
r.Route("/users", func(users chi.Router) {
users.Get("/", listUsers)
users.Post("/", createUser)
users.Route("/{id}", func(user chi.Router) {
user.Get("/", getUser)
user.Put("/", updateUser)
user.Delete("/", deleteUser)
})
})This structure mirrors the URL hierarchy: /users, /users/42, /users/42 with different methods. user.Get("/", getUser) inside the /{id} subrouter handles /users/42.
r.Group creates a new scope that uses certain middleware without changing the path:
r.Group(func(admin chi.Router) {
admin.Use(middleware.BasicAuth("app",
map[string]string{"admin": "rahasia"}))
admin.Get("/admin/stats", statsHandler)
})
r.Get("/public", publicHandler)The admin group uses Basic Auth, while the /public route outside the group is unaffected. r.Group(func(admin chi.Router){:go}... is the way to separate areas that need different protection without a separate prefix.
Use r.Group when:
Use r.Route when the paths genuinely differ and you want to clean them up from repeated prefixes.
r.Mount attaches another router — even someone else's — as a handler under a prefix:
func main() {
r := chi.NewRouter()
v1 := chi.NewRouter()
v1.Use(middleware.Recoverer)
v1.Get("/status", statusHandler)
r.Mount("/v1", v1)
http.ListenAndServe(":8080", r)
}r.Mount("/v1", v1) makes all routes in v1 reachable via /v1/status. The router v1 stands on its own with its own Recoverer middleware — it is not a child of r.
Because Mount accepts an http.Handler, you can attach anything:
fileServer := http.FileServer(http.Dir("./static"))
r.Mount("/assets", http.StripPrefix("/assets", fileServer))http.StripPrefix("/assets", fileServer) removes the prefix before forwarding to the file server — a pattern we'll reuse in episode 7 for static files.
For real projects, move each resource into its own router function:
func usersRouter() http.Handler {
users := chi.NewRouter()
users.Get("/", listUsers)
users.Post("/", createUser)
users.Route("/{id}", func(user chi.Router) {
user.Get("/", getUser)
user.Put("/", updateUser)
})
return users
}
func main() {
r := chi.NewRouter()
r.Mount("/users", usersRouter())
r.Mount("/products", productsRouter())
http.ListenAndServe(":8080", r)
}usersRouter() and productsRouter() each return an http.Handler, then get mounted at the root. Every resource now stands alone and is easy to test.
The combination of With and subrouters gives you precise middleware control:
r.Route("/api", func(api chi.Router) {
api.Use(middleware.Throttle(100))
api.Route("/admin", func(admin chi.Router) {
admin.Use(middleware.BasicAuth("app",
map[string]string{"root": "s3cret"}))
admin.Get("/dashboard", dashboardHandler)
})
api.With(middleware.Compress(5)).Get("/export", exportHandler)
})api.With(middleware.Compress(5)).Get("/export", ...) applies inline middleware to only one route — a pattern called inline middleware, strengthened further in version 5.3 as we'll discuss in episode 20.
Key takeaways:
r.Route creates a subrouter with a prefix and inherits the parent's middleware.r.Group scopes middleware without changing the path.r.Mount attaches another router or handler as a sub-app.http.Handler.With provides inline middleware per route.In the next episode 6 we focus on chi's backbone: middleware and chains — writing your own middleware, using r.Use, composing with middleware.Chain, and exploring built-in middleware like RequestID, Logger, Recoverer, and BasicAuth.