---
title: "Chi Router em Go: API REST Idiomática para Produção"
url: "https://golang.com.br/blog/chi-router-go-api-rest-producao/"
markdown_url: "https://golang.com.br/blog/chi-router-go-api-rest-producao.MD"
description: "Aprenda Chi Router em Go: rotas, parâmetros, grupos, middleware, erros, testes com httptest, observabilidade e estrutura de API REST para produção."
date: "2026-08-06"
author: "Golang Brasil"
---

# 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](/blog/frameworks-http-go-gin-echo-fiber-chi/). Para rever os fundamentos do protocolo e dos handlers, comece pelo [tutorial de API REST em Go](/aprenda/api-rest-go/).

## Resposta rápida: quando escolher Chi

| Cenário | Escolha inicial |
|---|---|
| API pequena com poucas rotas no Go 1.22+ | `net/http` puro |
| API modular, grupos e middleware por recurso | **Chi** |
| Time quer binding, validação e convenções prontas | Gin ou Echo |
| Biblioteca interna precisa aceitar `http.Handler` | **Chi** ou `net/http` |
| Migração gradual de uma API da standard library | **Chi** |
| Equipe já padronizou `gin.Context` e middleware Gin | Continue com Gin |
| Throughput de “hello world” é o único argumento | Meç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:

```bash
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`:

```go
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](/blog/graceful-shutdown-go-producao/).

## 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`:

```go
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

```go
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

```go
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

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

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

```go
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 é:

```text
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`:

```go
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:

```go
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](/blog/dependency-injection-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:

```go
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:

```go
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](/blog/json-go-encoding-json-v2-streaming-validacao/). 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:

```go
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ção | HTTP |
|---|---|
| Entrada sintaticamente inválida | `400 Bad Request` |
| Validação semântica falhou | `422 Unprocessable Entity` |
| Credencial ausente ou inválida | `401 Unauthorized` |
| Identidade válida sem permissão | `403 Forbidden` |
| Recurso não existe | `404 Not Found` |
| Conflito de unicidade/estado | `409 Conflict` |
| Limite excedido | `429 Too Many Requests` |
| Falha inesperada | `500 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](/aprenda/golang-erros/) 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.

```go
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](/blog/context-timeout-cancelamento-go/) e o guia dedicado de [middleware com net/http, Chi e Gin](/blog/middleware-go-net-http-chi-gin-producao/).

## 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:

```go
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](/blog/openapi-go-oapi-codegen-contratos/), em vez de depender só de exemplos no README.

## Testes com httptest sem abrir porta

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

```go
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](/blog/mocks-go-testify-gomock-fakes-httptest/) 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](/blog/go-opentelemetry-observabilidade-tracing-metricas/) com [logging estruturado usando slog](/blog/slog-go-logging-estruturado/) 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](/blog/health-checks-go-liveness-readiness-startup/).

## Chi vs net/http, Gin e Echo

| Critério | net/http | Chi | Gin/Echo |
|---|---|---|---|
| Tipo central | `http.Handler` | `http.Handler` | Contexto do framework |
| Path params | Nativo no Go 1.22+ | Sim | Sim |
| Grupos e sub-routers | Mais manual | Excelente | Excelente |
| Binding/validação | Manual | Manual | Mais pronto |
| Middleware standard library | Nativo | Nativo | Pode exigir adaptação |
| Acoplamento do handler | Mínimo | Mínimo | Maior |
| Curva para dev que conhece net/http | Nenhuma | Baixa | Baixa/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](/blog/rate-limiting-go-api-producao/) e [autenticação e autorização em Go](/blog/autenticacao-autorizacao-go-apis/).

## 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](/aprenda/api-rest-go/), o comparativo de [frameworks HTTP](/blog/frameworks-http-go-gin-echo-fiber-chi/) e o roadmap de [Go para backend](/aprenda/golang-para-backend/). Para transformar o projeto em experiência profissional, acompanhe também as [vagas Go no Brasil](/vagas/).
