Skip to content

What it is

chi is a lightweight, idiomatic HTTP router for Go that is fully compatible with net/http. Handlers are plain http.HandlerFunc, so any standard-library middleware works without adaptation.

chi provides a trie-based router with URL parameters, sub-routers, middleware stacks and route groups — while every handler remains a standard http.Handler. This means net/http middleware, testing with httptest, and any third-party handler all work unchanged.

Installation

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

Getting started

The smallest useful thing you can do with it, and what each part means.

Standard handlers, better routing
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.RequestID, middleware.RealIP, middleware.Recoverer)

    r.Get("/books/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "id")
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"id": id})
    })

    http.ListenAndServe(":8080", r)
}
The handler signature is exactly net/http's, so nothing about chi leaks into your business logic. Swapping routers later touches only the wiring.
Sub-routers for versioned APIs
r := chi.NewRouter()

r.Route("/api/v1", func(r chi.Router) {
    r.Route("/books", func(r chi.Router) {
        r.Get("/", listBooks)
        r.Post("/", createBooks)

        r.Route("/{bookID}", func(r chi.Router) {
            r.Use(BookCtx) // loads the book once for all child routes
            r.Get("/", getBook)
            r.Delete("/", deleteBook)
        })
    })
})
Nested Route calls express the URL hierarchy structurally, and middleware applied at a level covers everything beneath it. This is chi's most distinctive strength for larger APIs.

Advanced usage

Where the library earns its place over a simpler alternative.

Context middleware that loads a resource once
type ctxKey string

const bookKey ctxKey = "book"

func BookCtx(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        book, err := store.Find(r.Context(), chi.URLParam(r, "bookID"))
        if errors.Is(err, ErrNotFound) {
            http.Error(w, "not found", http.StatusNotFound)
            return
        } else if err != nil {
            http.Error(w, "internal error", http.StatusInternalServerError)
            return
        }

        ctx := context.WithValue(r.Context(), bookKey, book)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func getBook(w http.ResponseWriter, r *http.Request) {
    book := r.Context().Value(bookKey).(*Book)
    json.NewEncoder(w).Encode(book)
}
Every route under the sub-router gets the loaded resource without repeating the lookup. Use a private key type rather than a plain string so other packages cannot collide with your context keys.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

chi.URLParam returns an empty string
The parameter name does not match the pattern, or the handler is not mounted under a route that declares it. Check the braces in the route pattern match the name exactly.
Middleware appears to be ignored
chi panics if Use is called after routes are registered on the same router. Register all middleware first, or apply it inside a Group or Route block.

Best practices

  • Prefer chi when you want routing improvements without adopting a framework — everything stays http.Handler.
  • Use a distinct unexported type for context keys; plain strings risk collisions across packages.
  • Apply middleware with r.Use before defining routes on that router — middleware added afterwards will not apply.
  • Evaluate whether Go 1.22's standard router is now sufficient; for simple method-and-path routing it often is.

Background

Why it exists, and what it was reacting to.

chi was written by Peter Kieltyka at Pressly, built around the principle that a router should not require you to leave the standard library. Its context-based parameter passing was influential enough that Go 1.22's own router adopted a similar approach.