# go-chi/chi: a stdlib-first router for Go HTTP services

> chi is a small net/http-compatible router built on a radix trie, aimed at REST APIs that need to stay maintainable as they grow. It is not a framework, and that is the whole point.

**go-chi/chi** — lightweight, idiomatic and composable router for building Go HTTP services

- Repository: https://github.com/go-chi/chi
- Website: https://go-chi.io
- Stars: 22,907 · Forks: 2,429
- Language: Go
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/go-chi-chi

## What chi replaces, and who it is for

The default Go HTTP story gives you http.ServeMux. It matches paths, and that is roughly where the story ends. There is no middleware chain, no route groups, no URL parameters, no way to mount one router under another. Teams that need those things usually reach for a framework, and then inherit the framework's handler signature, its context type, and its release cycle.

chi takes the third path. The README describes it as a "lightweight, idiomatic and composable router for building Go HTTP services", and the design goal it names first is project structure and maintainability for large REST API services. Handlers stay plain func(w http.ResponseWriter, r *http.Request). Middleware stays func(http.Handler) http.Handler. Anything in the Go ecosystem written against net/http drops in unchanged, and nothing written for chi needs a chi-specific adapter to be understood by a stdlib reader.

The audience is narrow and identifiable: backend teams running several HTTP services, where the routing file is the thing that becomes unreadable at month eighteen. If your service has four endpoints, chi is more machinery than you need. If it has four hundred spread across resources, sub-resources and an admin surface, the composition primitives are the reason to be here.

## The radix trie and the Router interface

Under the surface, chi's router is described as a Patricia radix trie. That choice matters for two reasons. First, route lookup walks the tree by shared path prefix rather than scanning a list of patterns, so the cost of adding routes does not compound the way it does with naive matching. Second, the tree is what makes URL parameters and wildcards expressible in the pattern string itself.

On top of the tree sits the Router interface, which embeds http.Handler and http.Handler-compatible routing methods. The README lists four composition primitives that do the real work. Use appends middleware to the router's stack. With adds inline middleware for a single endpoint, which is how the README attaches a paginate handler to list routes without polluting the stack. Group creates an inline router along the current path with a fresh middleware stack. Route mounts a sub-router under a pattern string.

The distinction between Group and Route is the one people get wrong. Route nests a router under a path prefix and inherits the parent's middleware. Group does not change the URL path; it gives you a fresh middleware stack at the same path, which is what you want when a handful of routes need an extra guard but not a new prefix. The README's admin example takes the other route entirely, building a completely separate chi.NewRouter() and attaching it with r.Mount("/admin", adminRouter()).

## Installing chi and serving a first route

The README gives a single install command. Note the /v5 suffix: v5 is a separate module path, so the import in your source must match it.

```bash
go get -u github.com/go-chi/chi/v5
```

The README's minimal example is a complete main package. It creates a router, attaches the Logger middleware from the middleware subpackage, registers one GET handler, and hands the router to http.ListenAndServe on port 3000.

```go
package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

func main() {
	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("welcome"))
	})
	http.ListenAndServe(":3000", r)
}
```

Run it and request the root path. You should see the string welcome in the response body, and a log line from the Logger middleware on stdout. Nothing else is required: the router is an http.Handler, so any server, test harness or reverse proxy that accepts one will accept this.

The README's fuller REST preview adds the base middleware stack it recommends: RequestID, one of the ClientIPFrom* variants, Logger, Recoverer, and Timeout. The comment in the example is explicit that you pick one ClientIPFrom* based on your infrastructure, which is a hint that this is deployment-specific rather than a default you copy blindly.

```go
r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.ClientIPFromRemoteAddr) // pick one ClientIPFrom* based on your infra, see below
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
```

The Timeout middleware sets a deadline on the request context so that ctx.Done() fires and downstream handlers can stop work. That is a cancellation signal, not a hard kill; a handler that ignores its context will keep running.

## Sub-routers, URL parameters and context

The pattern that makes chi worth the import is the nested route block. The README's articles example mounts a resource at /articles, registers list and create handlers on it, then nests a second Route on /{articleID} for the single-article operations. Each nesting level can carry its own middleware, so ArticleCtx runs only for routes under /{articleID}.

That middleware is where chi's context story shows up. It reads the parameter with chi.URLParam(r, "articleID"), looks the article up, and attaches it with context.WithValue before calling next.ServeHTTP. The downstream handler pulls it back out with ctx.Value("article"). No framework-specific request object is involved; this is the standard library context package, which the README cites as the foundation chi is built on.

Two details in that example are worth copying and one is worth avoiding. Worth copying: the type assertion with the ok flag, which returns 422 when the value is missing rather than panicking. Also worth copying: r.With(paginate) on individual routes instead of r.Use(paginate) on the whole resource, so pagination logic applies only where it makes sense. Worth avoiding: the README uses a bare string as the context key. That works, but a package-private type is the safer convention, and the documentation does not push you either way.

Regex parameters are supported inline. The README registers /{articleSlug:[a-z-]+} for slug lookups, which keeps /articles/search from being swallowed by the slug route as long as the more specific route is registered. Route ordering is the reader's responsibility here; the README does not document a precedence guarantee for overlapping patterns.

## Where chi is the wrong tool

chi gives you routing and composition. It does not give you request binding, struct validation, an ORM, a dependency injection container, or a project skeleton. Teams coming from Gin or Echo often expect these and find the gap surprising. You will write your own JSON decode-and-validate helper, or import one, and you will decide your own directory layout. The README points at the _examples directory as documentation, which is an honest signal that the surrounding conventions are not specified by the project.

The middleware subpackage is optional and separate from the core router. The README puts the core at under 1000 LOC and lists middleware, render and docgen as add-ons, with render and docgen living in their own repositories. That means a middleware you rely on can version independently of the router.

The go.mod carries a go 1.24 directive with a comment stating that chi supports the four most recent major versions of Go. If your build environment is pinned to an older toolchain, that directive is the first thing to check, and it is not negotiable at the module level.

Finally, chi is a router, not a server. It does nothing about TLS, graceful shutdown, connection limits or observability beyond the middleware it ships. Those are net/http concerns and the project leaves them there.

## Go Chi vs Gin: the actual difference

The comparison people search for is Go Chi vs Gin, and the difference is architectural rather than a matter of speed claims. Gin ships its own context type. Handlers take *gin.Context, which carries the request, the response writer, parameter accessors and rendering helpers in one object. That is convenient and it is a dependency: your handlers, your middleware and your tests are written against Gin's types, and moving off Gin means rewriting them.

chi's handlers are net/http handlers. There is no chi context type; parameters come from chi.URLParam(r, "id") and values from the standard context package. Middleware is func(http.Handler) http.Handler, which is the same signature used by every net/http middleware in the ecosystem. The cost is that you assemble more yourself. The benefit is that the boundary between your code and the router is thin enough to remove.

If your team wants binding, validation and rendering included, Gin's approach removes boilerplate you would otherwise write. If your team already has net/http middleware, or expects to swap routers later, chi's stdlib-only stance is the deciding factor. The README states chi has no external dependencies beyond the standard library and net/http, which is the concrete form of that argument.

## Maintenance, testing and licence

The repository is not archived, and the last push was on 2026-09-18, three days before this writing. Recent releases are v5.3.0 on 2026-05-22, v5.3.1 on 2026-07-06 and v5.3.2 on 2026-08-20, so the release cadence over the last few months is steady. The module path is versioned at v5, which means a future v6 would be a new import path rather than an in-place break.

The Makefile is the upgrade-cost story. Running make test executes go clean -testcache followed by test-router and test-middleware, each of which runs go test -race -v against the root package and ./middleware respectively. The race detector is on by default in the project's own test target. For an adopter, that is a useful template: when you bump chi, run the same race-enabled tests against your own handlers, because middleware chaining and context values are exactly the kind of code where a race surfaces under load rather than in a unit test.

chi is MIT licensed. That is permissive and compatible with commercial use, but the LICENSE file is the authority and this is not legal advice. If you vendor the router, keep the licence text with it, and check the licences of the optional subpackages separately since render and docgen live in their own repositories.

## Conclusion

Adopt chi if your service is already written against net/http and you want routing, middleware chaining and sub-router composition without giving up stdlib handlers or the wider middleware ecosystem. Skip it if you want a full framework with binding, validation and an opinionated project layout, or if you need per-route regexes beyond what the pattern syntax covers. Before committing, verify three things in your own code: that your Go toolchain meets the go 1.24 directive in go.mod, that your middleware ordering matches the RequestID, ClientIPFromRemoteAddr, Logger, Recoverer, Timeout sequence the README shows, and that the routes you mount under r.Route and r.Mount are the ones you expect by running go test -race -v . from the Makefile.

## FAQ

### What does go-chi/chi do?

It is a lightweight, idiomatic and composable router for building Go HTTP services, built on a Patricia radix trie and fully compatible with net/http. It handles routing, middleware chaining, route groups and sub-router mounting while leaving handlers as standard http.HandlerFunc.

### What is a chi router?

The chi router is the core type you create with chi.NewRouter(). It implements the Router interface, which embeds http.Handler and adds Use, With, Group, Route and Mount for composing middleware and sub-routers, and it can be passed directly to http.ListenAndServe.

### What are the key differences between go-chi/chi and Gin?

Gin handlers take a *gin.Context that bundles request, response and rendering helpers, so your code depends on Gin's types. chi handlers are plain net/http handlers, parameters come from chi.URLParam and values from the standard context package, and the README states chi has no external dependencies beyond the standard library and net/http.

## Sources

- [go-chi/chi on GitHub](https://github.com/go-chi/chi)
- [License: MIT](https://github.com/go-chi/chi/blob/master/LICENSE)
- [Project website](https://go-chi.io)
- [README](https://github.com/go-chi/chi/blob/master/README.md)
- [Releases](https://github.com/go-chi/chi/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/go-chi-chi
