Como Criar uma API REST com Go
Resposta rápida: desde o Go 1.22, a standard library (net/http) já faz roteamento por método HTTP e por variável de caminho (GET /tasks/{id}) sem nenhuma biblioteca externa. Isso significa que você pode construir uma API REST de produção — rotas, CRUD, JSON, middleware, timeouts e graceful shutdown — usando só o que vem com a linguagem. Este tutorial mostra como, do zero ao deploy, com código pronto para copiar.
Go é uma das melhores linguagens para APIs REST: binário único e compilado, baixo consumo de memória, concorrência barata via goroutines e uma biblioteca padrão cobrindo HTTP, JSON e criptografia. Se você quer entender por que equipes escolhem Go para back-end, leia Go para back-end em 2026; aqui o foco é colocar uma API no ar.
Este é o caminho “sem framework”. Para a versão com o framework Gin, siga a série completa API REST com Golang e Gin — a lógica de domínio é a mesma, o que muda é a camada de roteamento.
O que vamos construir
Uma API de tarefas (TODO) com CRUD completo, persistência em memória, validação de entrada, middleware de log, timeouts e desligamento elegante. Ao final você saberá estruturar handlers, ler/escrever JSON com segurança e preparar a API para produção.
1. Setup do projeto
Crie o módulo e o primeiro arquivo:
mkdir todo-api
cd todo-api
go mod init todo-api
Você precisa do Go 1.22 ou superior (a versão atual do site é Go 1.26). Verifique com go version. Se ainda não domina módulos, leia Go Modules na prática.
2. Modelo e “banco de dados” em memória
Comece definindo o modelo de domínio e um armazenamento simples, protegido por um sync.Mutex (em produção você troca isso por PostgreSQL — veja o passo 7).
// main.go
package main
import (
"log"
"net/http"
)
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
type TaskStore struct {
mu sync.Mutex
tasks map[int]Task
nextID int
}
func NewTaskStore() *TaskStore {
return &TaskStore{tasks: make(map[int]Task), nextID: 1}
}
Por que o Mutex? Várias requisições concorrentes podem escrever no mapa ao mesmo tempo, e mapas em Go não são seguros para escrita concorrente. O Mutex serializa o acesso — um conceito que vale revisitar em concorrência em Go.
3. Rotas com Go 1.22+ (net/http)
A novidade do Go 1.22 é o ServeMux com padrões de método e variáveis de caminho. Em vez de fazer switch r.Method e cortar a URL à mão, você declara a rota já com método e {id}:
func main() {
store := NewTaskStore()
h := &TaskHandler{store: store}
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", h.list)
mux.HandleFunc("POST /tasks", h.create)
mux.HandleFunc("GET /tasks/{id}", h.get)
mux.HandleFunc("PUT /tasks/{id}", h.update)
mux.HandleFunc("DELETE /tasks/{id}", h.delete)
log.Println("servidor em http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Para ler o id dentro de um handler, use r.PathValue("id"). Se vier vazio ou inválido, retorne 400. Esse padrão elimina dezenas de linhas de parsing manual que tutors mais antigos ainda mostram.
4. JSON: entrada com validação, saída consistente
O erro mais comum em APIs Go é confiar cegamente no json.Decode. Em produção você quer: limitar o tamanho do corpo, rejeitar campos desconhecidos e validar o domínio. Veja o create:
func (h *TaskHandler) create(w http.ResponseWriter, r *http.Request) {
// Limite de 1 MB no corpo da requisição
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var in TaskInput
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // rejeita campos não esperados
if err := dec.Decode(&in); err != nil {
writeError(w, http.StatusBadRequest, "JSON inválido: "+err.Error())
return
}
if err := in.Validate(); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
task := h.store.Add(in.Title)
writeJSON(w, http.StatusCreated, task)
}
O TaskInput separa o DTO de entrada do modelo de domínio — uma prática que evita que campos como id sejam sobrescritos pelo cliente. Para um aprofundamento em tags, precisão numérica, streaming e encoding/json/v2, leia o guia JSON em Go.
As respostas sempre passam por um helper que centraliza cabeçalho e serialização:
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
Erros em formato {"error": "..."} são um contrato simples e previsível; se sua API crescer, padronize com problemas detalhados (RFC 7807) e um contrato OpenAPI.
5. CRUD completo (list, get, update, delete)
func (h *TaskHandler) list(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, h.store.All())
}
func (h *TaskHandler) get(w http.ResponseWriter, r *http.Request) {
id, ok := parseID(r)
if !ok {
writeError(w, http.StatusBadRequest, "ID inválido")
return
}
task, found := h.store.Get(id)
if !found {
writeError(w, http.StatusNotFound, "tarefa não encontrada")
return
}
writeJSON(w, http.StatusOK, task)
}
func (h *TaskHandler) update(w http.ResponseWriter, r *http.Request) {
id, ok := parseID(r)
if !ok {
writeError(w, http.StatusBadRequest, "ID inválido")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var in TaskInput
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&in); err != nil {
writeError(w, http.StatusBadRequest, "JSON inválido")
return
}
task, found := h.store.Update(id, in.Title, in.Completed)
if !found {
writeError(w, http.StatusNotFound, "tarefa não encontrada")
return
}
writeJSON(w, http.StatusOK, task)
}
func (h *TaskHandler) delete(w http.ResponseWriter, r *http.Request) {
id, ok := parseID(r)
if !ok {
writeError(w, http.StatusBadRequest, "ID inválido")
return
}
if !h.store.Delete(id) {
writeError(w, http.StatusNotFound, "tarefa não encontrada")
return
}
w.WriteHeader(http.StatusNoContent)
}
func parseID(r *http.Request) (int, bool) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil || id < 1 {
return 0, false
}
return id, true
}
| Operação | Método + rota | Sucesso | Falha comum |
|---|---|---|---|
| Listar | GET /tasks | 200 | — |
| Criar | POST /tasks | 201 | 400 (JSON inválido) |
| Buscar | GET /tasks/{id} | 200 | 404 |
| Atualizar | PUT /tasks/{id} | 200 | 404 |
| Remover | DELETE /tasks/{id} | 204 | 404 |
6. Middleware, timeouts e graceful shutdown
A versão acima usa http.ListenAndServe, que não impõe timeouts — em produção isso vira vazamento de goroutines e DoS acidental. O correto é construir um http.Server com limites e um desligamento elegante que termina as requisições em andamento:
func main() {
store := NewTaskStore()
h := &TaskHandler{store: store}
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", h.list)
mux.HandleFunc("POST /tasks", h.create)
mux.HandleFunc("GET /tasks/{id}", h.get)
mux.HandleFunc("PUT /tasks/{id}", h.update)
mux.HandleFunc("DELETE /tasks/{id}", h.delete)
handler := logging(mux) // middleware de log por fora
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Roda em segundo plano e aguarda SIGINT/SIGTERM para desligar
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
<-ctx.Done()
log.Println("desligando...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
log.Println("servidor parado")
}
Um middleware de log simples envolve o handler:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rec, r)
log.Printf("%s %s %d %s", r.Method, r.URL.Path, rec.status, time.Since(start))
})
}
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
Para logs estruturados com níveis e campos, troque o log padrão pelo pacote slog — veja slog: logging estruturado para produção.
7. Trocando memória por PostgreSQL
O TaskStore em memória existe só para o tutorial. Para dados reais, conecte ao PostgreSQL com pgx ou sqlc:
- Go com PostgreSQL (pgxpool) — pool de conexões e queries seguras
- sqlc: SQL type-safe em Go — gere código Go a partir de SQL
- Transações, locks e retry no PostgreSQL — quando os dados começam a concorrer
Com banco de dados você também precisa de migrations — leia migrations em Go para produção.
8. Testando a API
Teste os handlers em isolamento com httptest, sem subir servidor. O padrão table-driven cobre vários casos com pouco código:
func TestCreateTask(t *testing.T) {
cases := []struct {
name string
body string
want int
}{
{"valido", `{"title":"Aprender Go","completed":false}`, http.StatusCreated},
{"sem titulo", `{"completed":false}`, http.StatusBadRequest},
{"campo desconhecido", `{"title":"x","extra":1}`, http.StatusBadRequest},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("POST", "/tasks", strings.NewReader(tc.body))
rec := httptest.NewRecorder()
newHandler().ServeHTTP(rec, req) // newHandler monta mux+store de teste
if rec.Code != tc.want {
t.Fatalf("status %d, esperado %d", rec.Code, tc.want)
}
})
}
}
Aprofunde-se em testes em Go e no guia de table-driven tests.
9. Testando com cURL
Suba com go run . e exercite os endpoints:
# Criar tarefa
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Aprender Go", "completed": false}'
# Listar
curl http://localhost:8080/tasks
# Buscar por ID
curl http://localhost:8080/tasks/1
# Atualizar
curl -X PUT http://localhost:8080/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "Aprender Go", "completed": true}'
# Remover
curl -X DELETE http://localhost:8080/tasks/1
10. Checklist de produção
Antes de expor a API, cubra estes pontos — cada um tem conteúdo aprofundado no site:
- Autenticação com JWT: autenticação JWT em Go
- Rate limiting para evitar abuso: rate limiting para APIs
- Observabilidade (métricas, traces, logs): observabilidade em Go
- Idempotência em escritas: webhooks, assinatura e idempotência
- Tratamento de erros consistente: erros em Go
- Arquitetura à medida que cresce: clean architecture em Go e microsserviços
- Documentação do contrato: OpenAPI com oapi-codegen
11. Quando usar Gin (e quando não)
A standard library resolve a maioria das APIs pequenas e médias. Frameworks como Gin valem a pena quando você quer binding por tags, validação declarativa (binding:"required"), middlewares maduros e uma sintaxe ainda mais concisa. O custo é uma dependência externa e mais convenções para aprender. Para decidir e ver o caminho com framework, siga a série API REST com Golang e Gin (parte 1 a 4).
12. Deploy com Docker
Multi-stage build mantém a imagem final pequena (só o binário):
# build
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o server .
# runtime
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
docker build -t todo-api .
docker run -p 8080:8080 todo-api
Para além do container local — Kubernetes, deploy em nuvem e CI/CD — veja Go com Docker, Go no Kubernetes e Go com TDD e CI/CD.
Próximos passos
A API funciona, mas é o começo. Uma progressão natural para dominar back-end com Go:
- Banco de dados real: conecte ao PostgreSQL com pgxpool e adicione migrations.
- Testes sólidos: cubra os handlers com table-driven tests.
- Segurança: adicione JWT e rate limiting.
- Arquitetura: evolua para clean architecture quando o domínio crescer.
- Carreira: pratique com perguntas de entrevista de Go e acompanhe vagas de back-end Go.
Veja Também
- Go para Iniciantes — fundamentos da linguagem
- Go para Back-end em 2026 — por que Go domina o back-end
- Curso Gratuito de Golang — trilha do zero ao projeto
- API REST com Gin (série completa) — versão com framework
- JSON em Go — tags, validação e streaming
- Cheatsheet de Sintaxe Go — referência rápida
- Vagas de Go — oportunidades no Brasil
Última atualização: Julho 2026 — roteamento atualizado para Go 1.22+ (ServeMux com método e PathValue), timeouts, graceful shutdown e checklist de produção.