← Voltar para o blog

Chi Router em Go: API REST Idiomática para Produção

Aprenda Chi Router em Go: rotas, parâmetros, grupos, middleware, erros, testes com httptest, observabilidade e estrutura de API REST para produção.

Chi Router é uma das melhores escolhas para construir API REST em Go quando você quer mais organização que o net/http puro, mas não quer abandonar o modelo idiomático de http.Handler. Ele adiciona parâmetros de URL, grupos, sub-roteadores e composição de middleware, mantendo handlers, testes e bibliotecas compatíveis com a standard library. Para APIs pequenas, o roteamento nativo do Go 1.22+ pode bastar; para serviços modulares, Chi costuma oferecer o meio-termo mais equilibrado.

Este guia mostra como usar github.com/go-chi/chi/v5 em uma API de produção: estrutura de projeto, rotas, parâmetros, grupos, middleware, JSON, erros, testes com httptest, timeouts, observabilidade e graceful shutdown. Se você ainda está escolhendo a stack, compare antes Gin, Echo, Fiber, Chi e net/http. Para rever os fundamentos do protocolo e dos handlers, comece pelo tutorial de API REST em Go.

Resposta rápida: quando escolher Chi

CenárioEscolha inicial
API pequena com poucas rotas no Go 1.22+net/http puro
API modular, grupos e middleware por recursoChi
Time quer binding, validação e convenções prontasGin ou Echo
Biblioteca interna precisa aceitar http.HandlerChi ou net/http
Migração gradual de uma API da standard libraryChi
Equipe já padronizou gin.Context e middleware GinContinue com Gin
Throughput de “hello world” é o único argumentoMeça a aplicação real antes de escolher

A principal vantagem do Chi não é “ser o router mais rápido”. É preservar o contrato do ecossistema HTTP de Go. Um handler Chi continua sendo um http.Handler; um middleware continua podendo ter a assinatura func(http.Handler) http.Handler; testes continuam usando httptest; instrumentações de net/http continuam encaixando sem reescrever a aplicação.

Instalação e servidor mínimo

Crie o módulo e instale a versão principal atual do pacote:

go mod init example.com/catalogo
go get github.com/go-chi/chi/v5

Um servidor mínimo pode usar recursos do Chi sem esconder a configuração importante do http.Server:

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"time"

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

func main() {
	r := chi.NewRouter()
	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)
	r.Use(middleware.Recoverer)

	r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
	})

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           r,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       15 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       60 * time.Second,
	}

	log.Printf("servidor ouvindo em %s", srv.Addr)
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
}

Os timeouts precisam refletir o comportamento da aplicação. Não copie valores sem considerar upload, streaming ou endpoints longos. O ponto é não depender dos zeros do http.Server por acidente. Para encerrar o processo sem cortar requisições em andamento, use o padrão completo de graceful shutdown em Go.

Rotas, métodos e parâmetros de URL

Chi oferece métodos como Get, Post, Put, Patch e Delete, além de Method e Handle quando você precisa de controle explícito. Parâmetros declarados com {nome} são lidos por chi.URLParam:

func produtoPorID(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "produtoID")
	if id == "" {
		http.Error(w, "produto não informado", http.StatusBadRequest)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]string{
		"id":   id,
		"nome": "Teclado mecânico",
	})
}

func routes() http.Handler {
	r := chi.NewRouter()
	r.Get("/produtos/{produtoID}", produtoPorID)
	return r
}

Não confunda “a rota casou” com “o parâmetro é válido”. O router extrai texto; seu handler ou camada de entrada ainda precisa validar UUID, número, tamanho e formato. Se o ID for inteiro, use strconv.ParseInt e responda 400 Bad Request quando o valor for inválido. Se o recurso válido não existir, responda 404 Not Found.

Também evite colocar regras de negócio dentro da função HTTP. O handler deve traduzir protocolo: ler path, query, headers e body; chamar um caso de uso; mapear o resultado para status e JSON.

Organize a API com Route, Group, With e Mount

Em uma aplicação real, o valor do Chi aparece quando as rotas deixam de ser uma lista plana.

Route: prefixo e sub-roteador

r.Route("/api/v1/produtos", func(r chi.Router) {
	r.Get("/", listarProdutos)
	r.Post("/", criarProduto)
	r.Get("/{produtoID}", buscarProduto)
	r.Put("/{produtoID}", atualizarProduto)
	r.Delete("/{produtoID}", removerProduto)
})

Group: conjunto isolado de middleware

r.Group(func(r chi.Router) {
	r.Use(autenticar)
	r.Get("/me", perfilAtual)
	r.Get("/pedidos", listarMeusPedidos)
})

With: middleware só para uma rota ou pequeno conjunto

r.With(exigirPapel("admin")).Delete(
	"/api/v1/usuarios/{usuarioID}",
	removerUsuario,
)

Mount: módulo que já expõe http.Handler

r.Mount("/api/v1/produtos", produtoHandler.Routes())
r.Mount("/api/v1/pedidos", pedidoHandler.Routes())

Mount é útil em monólitos modulares: cada domínio constrói seu sub-router, recebe dependências pelo construtor e expõe apenas um http.Handler. Evite, porém, criar um router global importado por todos os pacotes. Isso esconde dependências, dificulta testes e transforma registro de rotas em efeito colateral.

Uma estrutura de projeto simples e testável

Não existe uma árvore obrigatória para Chi. Uma estrutura razoável para uma API média é:

cmd/api/main.go
internal/httpapi/router.go
internal/httpapi/respond.go
internal/produto/handler.go
internal/produto/service.go
internal/produto/repository.go
internal/postgres/produto_repository.go

A montagem fica explícita no main:

repo := postgres.NewProdutoRepository(db)
service := produto.NewService(repo, logger)
handler := produto.NewHandler(service, logger)
router := httpapi.NewRouter(handler, logger)

E o router recebe módulos prontos:

func NewRouter(produtos *produto.Handler, logger *slog.Logger) http.Handler {
	r := chi.NewRouter()
	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)
	r.Use(middleware.Recoverer)
	r.Use(requestLogger(logger))

	r.Get("/healthz", healthz)
	r.Mount("/api/v1/produtos", produtos.Routes())
	return r
}

Esse desenho combina com dependency injection em Go sem framework e mantém o bootstrap legível. Chi organiza a camada HTTP; ele não deve construir banco, carregar variável de ambiente ou decidir implementação de repositório.

JSON de entrada: limite, decode e validação

Chi não faz binding automático. Isso parece trabalho extra, mas deixa a fronteira explícita. Um decoder seguro deve limitar o corpo, rejeitar campos desconhecidos quando o contrato exigir e impedir múltiplos objetos JSON concatenados:

func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB

	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()

	if err := dec.Decode(dst); err != nil {
		return fmt.Errorf("json inválido: %w", err)
	}

	if dec.Decode(&struct{}{}) != io.EOF {
		return errors.New("o body deve conter um único objeto JSON")
	}
	return nil
}

No handler:

type criarProdutoInput struct {
	Nome      string `json:"nome"`
	PrecoCent int64  `json:"preco_centavos"`
}

func (h *Handler) criar(w http.ResponseWriter, r *http.Request) {
	var input criarProdutoInput
	if err := decodeJSON(w, r, &input); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
		return
	}

	if strings.TrimSpace(input.Nome) == "" || input.PrecoCent <= 0 {
		writeError(w, http.StatusUnprocessableEntity, "validation_error", "nome e preço são obrigatórios")
		return
	}

	produto, err := h.service.Create(r.Context(), input.Nome, input.PrecoCent)
	if err != nil {
		h.writeServiceError(w, r, err)
		return
	}

	writeJSON(w, http.StatusCreated, produto)
}

Para uma discussão completa sobre DTOs, números, streaming e contratos, leia JSON em Go com encoding/json. Não exponha structs internas do banco como contrato público só para economizar um tipo.

Respostas e erros consistentes

Centralize a escrita de JSON para não esquecer Content-Type nem escrever status depois do body:

func writeJSON(w http.ResponseWriter, status int, value any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(value)
}

func writeError(w http.ResponseWriter, status int, code, message string) {
	writeJSON(w, status, map[string]any{
		"error": map[string]string{
			"code":    code,
			"message": message,
		},
	})
}

Mapeie erros conhecidos da camada de negócio com errors.Is:

Erro de aplicaçãoHTTP
Entrada sintaticamente inválida400 Bad Request
Validação semântica falhou422 Unprocessable Entity
Credencial ausente ou inválida401 Unauthorized
Identidade válida sem permissão403 Forbidden
Recurso não existe404 Not Found
Conflito de unicidade/estado409 Conflict
Limite excedido429 Too Many Requests
Falha inesperada500 Internal Server Error

Não devolva SQL, stack trace ou mensagem interna para o cliente. Registre o erro completo com request ID e devolva um código público estável. O guia de tratamento de erros em Go aprofunda errors.Is, errors.As, wrapping com %w e erros sentinela.

Middleware no Chi: ordem e escopo

Chi aceita qualquer middleware no padrão func(http.Handler) http.Handler. Uma ordem típica é:

  1. request ID;
  2. IP real, somente com proxies confiáveis;
  3. recovery;
  4. logging e métricas;
  5. timeout, quando adequado ao endpoint;
  6. CORS;
  7. autenticação;
  8. autorização e regras específicas do grupo.
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Use(requestLogger(logger))
r.Use(middleware.Timeout(30 * time.Second))

A ordem afeta o que cada camada consegue observar. O logger precisa enxergar o request ID; o recovery deve cobrir handlers e middlewares internos; autenticação não deve ser global se /healthz e /metrics precisam continuar acessíveis às probes.

Há outro cuidado: timeout via contexto não interrompe magicamente uma query ou chamada externa que ignora r.Context(). Propague o contexto para banco, HTTP client e filas. Leia context, timeout e cancelamento em Go e o guia dedicado de middleware com net/http, Chi e Gin.

NotFound e MethodNotAllowed com o mesmo contrato

Uma API não deve responder HTML padrão em alguns erros e JSON em outros. Configure os handlers do router:

r.NotFound(func(w http.ResponseWriter, r *http.Request) {
	writeError(w, http.StatusNotFound, "not_found", "rota não encontrada")
})

r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
	writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "método não permitido")
})

Isso também melhora testes e clientes gerados: o envelope de erro continua previsível. Se a API é pública, formalize rotas, schemas e códigos com OpenAPI e oapi-codegen, em vez de depender só de exemplos no README.

Testes com httptest sem abrir porta

Como Chi implementa http.Handler, o teste HTTP é direto:

func TestBuscarProduto(t *testing.T) {
	service := &fakeService{
		produto: Produto{ID: "prod_123", Nome: "Teclado"},
	}
	handler := NewHandler(service, slog.New(slog.NewTextHandler(io.Discard, nil)))
	router := handler.Routes()

	req := httptest.NewRequest(http.MethodGet, "/prod_123", nil)
	res := httptest.NewRecorder()

	router.ServeHTTP(res, req)

	if res.Code != http.StatusOK {
		t.Fatalf("status = %d; esperado %d", res.Code, http.StatusOK)
	}

	var got Produto
	if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
		t.Fatal(err)
	}
	if got.ID != "prod_123" {
		t.Fatalf("id = %q; esperado prod_123", got.ID)
	}
}

Cubra pelo menos:

  • caminho feliz;
  • parâmetro inválido;
  • body acima do limite;
  • JSON malformado e campo desconhecido;
  • recurso inexistente;
  • conflito;
  • autenticação ausente;
  • método e rota inexistentes;
  • cancelamento do contexto;
  • panic convertido em 500 pelo recovery.

Use testes de tabela para repetir casos sem duplicação e deixe integração com PostgreSQL para uma camada separada. O artigo de mocks, fakes e httptest em Go ajuda a escolher entre fake manual, mock gerado e servidor HTTP de teste.

Observabilidade e operação

O router é a melhor fronteira para iniciar telemetria HTTP, mas não pare nele. Uma API Chi de produção deve medir:

  • quantidade de requests por rota, método e status;
  • duração por rota, sem usar a URL bruta como label;
  • requests em andamento;
  • tamanho de request e response quando útil;
  • panics recuperados;
  • timeout e cancelamento;
  • saturação de pool do banco e chamadas downstream.

Use o padrão da rota, não /produtos/prod_123, como atributo de baixa cardinalidade. IDs em labels de Prometheus ou nomes de span explodem séries e custo. Combine OpenTelemetry em Go com logging estruturado usando slog e request ID.

Health checks também devem ser deliberados: liveness responde se o processo vive; readiness indica se pode receber tráfego. Não faça uma consulta pesada ao banco em toda liveness. Veja health checks em Go para Kubernetes.

Chi vs net/http, Gin e Echo

Critérionet/httpChiGin/Echo
Tipo centralhttp.Handlerhttp.HandlerContexto do framework
Path paramsNativo no Go 1.22+SimSim
Grupos e sub-routersMais manualExcelenteExcelente
Binding/validaçãoManualManualMais pronto
Middleware standard libraryNativoNativoPode exigir adaptação
Acoplamento do handlerMínimoMínimoMaior
Curva para dev que conhece net/httpNenhumaBaixaBaixa/média

Escolha net/http quando ele já deixa a aplicação simples. Escolha Chi quando você quer modularidade e composição sem criar uma ilha de framework. Escolha Gin ou Echo quando o time valoriza suas conveniências, já tem padrões internos e aceita o contexto próprio.

Não migre uma API estável só porque outro router ganhou benchmark. Meça gargalos reais: banco, serialização, chamadas externas e lock contention quase sempre dominam o custo. Uma migração faz sentido por manutenção, ergonomia, padronização ou compatibilidade — não por uma barra de throughput isolada.

Checklist de produção para uma API com Chi

Antes do deploy, confirme:

  • http.Server tem timeouts deliberados;
  • processo implementa graceful shutdown;
  • body JSON tem limite de tamanho;
  • handlers propagam r.Context();
  • erros públicos não vazam detalhes internos;
  • autenticação e autorização estão separadas;
  • rate limit protege login e endpoints caros;
  • request ID, logs, métricas e tracing estão integrados;
  • /healthz e readiness não estão atrás de auth por engano;
  • CORS usa origens explícitas quando há navegador;
  • testes cobrem 404, 405, 400, 401, 403, 409, 422 e 500 relevantes;
  • documentação OpenAPI acompanha o comportamento real;
  • nenhuma métrica usa ID ou URL bruta como label;
  • dependências chegam por construtor, não por variável global.

Rate limit, autenticação e autorização merecem políticas próprias; continue com rate limiting em APIs Go e autenticação e autorização em Go.

Perguntas frequentes

O que é Chi Router em Go?

Chi é um roteador HTTP leve e composável construído sobre net/http. Ele adiciona parâmetros, grupos, sub-roteadores e uma API conveniente de middleware sem substituir http.Handler por um tipo proprietário.

Quando usar Chi em vez de net/http puro?

Use net/http puro quando o roteamento nativo deixa a API clara. Adote Chi quando grupos, módulos montáveis, middleware por recurso e organização de rotas começam a exigir código manual demais, mas você quer preservar compatibilidade com a standard library.

Chi é melhor que Gin para API REST?

Depende do time. Chi favorece handlers portáveis, composição e controle. Gin oferece binding, validação integrada e mais convenções prontas. A melhor escolha é a que reduz complexidade total sem esconder o comportamento operacional.

Como testar rotas Chi em Go?

Monte o router com dependências fake, crie a requisição com httptest.NewRequest, grave a resposta com httptest.NewRecorder e chame ServeHTTP. Não é necessário abrir porta nem iniciar um processo separado.

Chi serve para APIs de produção?

Sim. Chi é usado como camada de roteamento, mas produção exige também timeouts, shutdown, limites, segurança, observabilidade e testes. O router ajuda a organizar essas preocupações; ele não substitui a engenharia do serviço.

Conclusão

Chi Router ocupa um espaço muito útil no ecossistema Go: entrega rotas e composição suficientes para APIs grandes sem afastar a aplicação de net/http. Comece pela standard library, adicione Chi quando a organização justificar e mantenha o domínio independente do router.

Uma base sólida usa handlers finos, serviços injetados por construtor, JSON limitado e validado, erros consistentes, middleware por escopo, testes com httptest e operação observável. Depois de implementar o primeiro módulo, revise o guia de API REST em Go, o comparativo de frameworks HTTP e o roadmap de Go para backend. Para transformar o projeto em experiência profissional, acompanhe também as vagas Go no Brasil.