O que é Chi e Por que Ele Importa
Chi é um roteador HTTP leve e expressivo para Go que se destaca por sua arquitetura idiomática e suporte nativo a middlewares compostos. Diferente de outros frameworks web em Go que tentam replicar patterns de linguagens dinâmicas, Chi foi construído desde o início respeitando os princípios de simplicidade e composição que caracterizam a linguagem. Ele implementa a interface http.Handler, o que significa que qualquer código Chi é compatível com o ecossistema padrão da biblioteca net/http do Go.
A proposta central do Chi é oferecer um roteador que não adicione overhead desnecessário, mas que permita escrever rotas de forma clara e com suporte primeiro para middlewares. Você não precisa aprender um "dialeto" especial ou magic frameworks — apenas Go puro, com uma camada de abstração bem pensada. Isso torna Chi particularmente atrativo para quem valoriza legibilidade, testabilidade e performance em aplicações web.
Conceitos Fundamentais do Roteamento
Estrutura Básica de uma Aplicação Chi
Uma aplicação Chi começa com a criação de um roteador via chi.NewRouter(). Este objeto é responsável por registrar rotas e middlewares, e pode ser passado diretamente para http.ListenAndServe() porque implementa http.Handler.
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
// Registrar uma rota simples GET
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Olá, mundo!")
})
http.ListenAndServe(":3000", r)
}
Neste exemplo, estamos criando um roteador que escuta na porta 3000 e responde com uma mensagem na rota raiz. Note que a função handler segue a assinatura padrão func(w http.ResponseWriter, r *http.Request), não há nada proprietário aqui.
Parâmetros de Rota (Route Parameters)
Chi permite capturar parâmetros dinâmicos na URL através da sintaxe {nomeDoParâmetro}. Esses valores são extraídos e podem ser acessados usando chi.URLParam().
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
// Rota com parâmetro dinâmico
r.Get("/users/{userID}", func(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"user_id": "%s"}`, userID)
})
// Rota com múltiplos parâmetros
r.Get("/posts/{postID}/comments/{commentID}", func(w http.ResponseWriter, r *http.Request) {
postID := chi.URLParam(r, "postID")
commentID := chi.URLParam(r, "commentID")
fmt.Fprintf(w, "Post %s, Comentário %s", postID, commentID)
})
http.ListenAndServe(":3000", r)
}
Chi não força tipagem de parâmetros — você recebe strings e decide como validar ou converter. Isso oferece controle fino, embora exija disciplina do desenvolvedor.
Agrupamento de Rotas (Route Groups)
Para aplicações que crescem, é essencial organizar rotas em grupos lógicos. Chi permite isso através do método Group(), que retorna um novo roteador aninhado sobre o qual você pode registrar subrotas e middlewares específicos.
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
// Grupo de rotas para API v1
r.Route("/api/v1", func(r chi.Router) {
r.Get("/status", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "API v1 está ativa")
})
// Subrotas para usuários
r.Route("/users", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Lista de usuários")
})
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Usuário criado")
})
r.Get("/{userID}", func(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
fmt.Fprintf(w, "Usuário %s", userID)
})
})
})
http.ListenAndServe(":3000", r)
}
Este padrão permite que você estruture sua API de forma hierárquica, refletindo a organização conceitual das suas funcionalidades. Cada grupo pode ter seus próprios middlewares, como veremos adiante.
Middlewares: O Coração da Composição
Entendendo Middlewares em Go
Um middleware em Go é simplesmente uma função que recebe um http.Handler e retorna outro http.Handler. Essa estrutura permite que você "envolva" handlers com lógica transversal como autenticação, logging, compressão, etc. Chi facilita o registro e composição desses middlewares.
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/go-chi/chi/v5"
)
// Middleware para logar requisições
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("Iniciando %s %s", r.Method, r.RequestURI)
next.ServeHTTP(w, r)
log.Printf("Concluído em %v", time.Since(start))
})
}
// Middleware para verificar autorização (exemplo simplificado)
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Token ausente", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
r := chi.NewRouter()
// Registrar middlewares globalmente (aplicados em todas as rotas)
r.Use(LoggingMiddleware)
r.Get("/public", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Rota pública")
})
// Grupo protegido com middleware específico
r.Route("/protected", func(r chi.Router) {
r.Use(AuthMiddleware)
r.Get("/data", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Dados privados")
})
})
http.ListenAndServe(":3000", r)
}
Note como Use() registra um middleware, e quando você chama Use() dentro de um Route(), o middleware aplica-se apenas àquele grupo e suas subrotas. Isso é composição idiomática em Go.
Middlewares Compostos e Contexto
Chi integra-se perfeitamente com context.Context, permitindo que middlewares transmitam dados para handlers através do contexto da requisição. Esse padrão evita dependências globais e torna o fluxo de dados explícito.
package main
import (
"context"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
// Tipo para armazenar valores no contexto
type ctxKey string
const userIDKey ctxKey = "userID"
// Middleware que extrai e valida um ID de usuário
func UserExtractorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-ID")
if userID == "" {
http.Error(w, "X-User-ID obrigatório", http.StatusBadRequest)
return
}
// Armazenar o userID no contexto
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
r := chi.NewRouter()
r.Route("/api", func(r chi.Router) {
r.Use(UserExtractorMiddleware)
r.Get("/profile", func(w http.ResponseWriter, r *http.Request) {
// Recuperar o userID do contexto
userID := r.Context().Value(userIDKey).(string)
fmt.Fprintf(w, "Perfil do usuário: %s", userID)
})
})
http.ListenAndServe(":3000", r)
}
Este padrão é fundamental em Go moderno. O contexto flui através de toda a cadeia de middleware e handler, permitindo que lógica compartilhada seja injetada sem acoplamento.
Middlewares de Terceiros e Padrões Comuns
Chi é compatível com middlewares populares do ecossistema Go. A comunidade mantém um conjunto de middlewares úteis e testados que podem ser usados diretamente.
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Middlewares do próprio Chi para tarefas comuns
r.Use(middleware.RequestID) // Gera ID único para cada requisição
r.Use(middleware.RealIP) // Extrai IP real (útil com proxies)
r.Use(middleware.Logger) // Logging estruturado
r.Use(middleware.Recoverer) // Recupera-se de panics
r.Get("/test", func(w http.ResponseWriter, r *http.Request) {
// RequestID está disponível via middleware.GetReqID(r.Context())
reqID := middleware.GetReqID(r.Context())
fmt.Fprintf(w, "Request ID: %s", reqID)
})
http.ListenAndServe(":3000", r)
}
Os middlewares embutidos no Chi (github.com/go-chi/chi/v5/middleware) cobrem cenários comuns como logging, recuperação de erros e extração de metadados de requisição. Você pode combinar esses com seus próprios middlewares.
Padrões Avançados e Organização
RESTful API Completa com Chi
Chi brilha quando você precisa construir APIs RESTful bem estruturadas. Combinando agrupamento de rotas com middlewares, é possível criar aplicações limpas e manuteníveis.
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// Simulação de banco de dados
var users = map[string]User{
"1": {ID: "1", Name: "Alice", Email: "alice@example.com"},
"2": {ID: "2", Name: "Bob", Email: "bob@example.com"},
}
func GetUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
users[user.ID] = user
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func GetUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
user, exists := users[userID]
if !exists {
http.Error(w, "Usuário não encontrado", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func UpdateUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
var user User
json.NewDecoder(r.Body).Decode(&user)
user.ID = userID
users[userID] = user
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func DeleteUser(w http.ResponseWriter, r *http.Request) {
userID := chi.URLParam(r, "userID")
delete(users, userID)
w.WriteHeader(http.StatusNoContent)
}
func main() {
r := chi.NewRouter()
// Middlewares globais
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// Rotas de healthcheck
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "OK")
})
// API v1
r.Route("/api/v1", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", GetUsers)
r.Post("/", CreateUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", GetUser)
r.Put("/", UpdateUser)
r.Delete("/", DeleteUser)
})
})
})
http.ListenAndServe(":3000", r)
}
Neste exemplo, a estrutura RESTful é clara: usuários podem ser listados, criados, recuperados individualmente, atualizados e deletados. Cada operação tem seu próprio handler, e as rotas refletem perfeitamente a semântica HTTP.
Middlewares Reutilizáveis com Dependências
Em aplicações reais, middlewares frequentemente precisam acessar serviços ou configurações. Chi permite criar middlewares que aceitam parâmetros e retornam o middleware propriamente dito.
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
// Simular um serviço de autenticação
type AuthService struct {
apiKey string
}
func (as *AuthService) ValidateToken(token string) bool {
return token == as.apiKey
}
// Factory para criar middleware com injeção de dependência
func AuthMiddlewareFactory(authService *AuthService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !authService.ValidateToken(token) {
http.Error(w, "Token inválido", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
// Middleware que injeta um banco de dados no contexto
func DatabaseMiddleware(db *sql.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "db", db)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func main() {
// Simular inicialização de serviços
authService := &AuthService{apiKey: "secret123"}
// db := connectToDatabase() // em uma aplicação real
r := chi.NewRouter()
// Registrar middlewares factory com dependências
r.Use(AuthMiddlewareFactory(authService))
// r.Use(DatabaseMiddleware(db))
r.Get("/protected", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Recurso protegido acessado com sucesso")
})
http.ListenAndServe(":3000", r)
}
Este padrão é essencial em aplicações que crescem: a injeção de dependência garante que seus middlewares permaneçam testáveis e desacoplados da implementação específica dos serviços.
Conclusão
Aprender Chi significa aprender a escrever código Go idiomático para construir APIs web. Os três pontos principais que você deve levar desta leitura são: primeiro, roteamento em Chi é simples e composável porque respeita a interface padrão http.Handler, mantendo compatibilidade com todo o ecossistema Go; segundo, middlewares são funções de primeira classe que permitem cruzar responsabilidades (autenticação, logging, validação) de forma elegante e reutilizável através do contexto; terceiro, a organização em grupos de rotas (Route()) aliada ao suporte a middlewares por grupo permite que você cresça de uma aplicação simples para uma API complexa sem refatorações radicais.
Chi não é um framework monolítico — é uma ferramenta focada que você compõe com outras bibliotecas do Go. Isso exige disciplina, mas oferece flexibilidade e clareza que frameworks "tudo incluído" dificilmente conseguem.