Janeiro 2026 · ~9 min

Como Criar uma API REST com Go em 2026

Crie uma API REST com Go usando a standard library (Go 1.22+): rotas com método, CRUD, JSON com validação, middleware, timeouts, graceful shutdown e deploy.

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çãoMétodo + rotaSucessoFalha comum
ListarGET /tasks200
CriarPOST /tasks201400 (JSON inválido)
BuscarGET /tasks/{id}200404
AtualizarPUT /tasks/{id}200404
RemoverDELETE /tasks/{id}204404

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:

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:

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:

  1. Banco de dados real: conecte ao PostgreSQL com pgxpool e adicione migrations.
  2. Testes sólidos: cubra os handlers com table-driven tests.
  3. Segurança: adicione JWT e rate limiting.
  4. Arquitetura: evolua para clean architecture quando o domínio crescer.
  5. Carreira: pratique com perguntas de entrevista de Go e acompanhe vagas de back-end Go.

Veja Também


Ú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.

Perguntas frequentes

Go precisa de framework para criar API REST?

Não. A partir do Go 1.22, a standard library (net/http) já faz roteamento por método e por variável de caminho (GET /tasks/{id}) sem nenhuma biblioteca externa. Frameworks como Gin e Echo continuam úteis para conveniências e ecossistema, mas não são mais necessários para montar uma API REST organizada.

Como rotear por método e por ID na standard library do Go?

Use http.NewServeMux() com padrões como mux.HandleFunc("GET /tasks/{id}", handler) e leia o parâmetro com r.PathValue("id"). Esse recurso entrou no Go 1.22 e elimina o parsing manual de URL.

Go é bom para criar APIs?

Excelente. Go é uma das melhores linguagens para APIs: compila rápido, consome pouca memória, suporta alta concorrência nativa com goroutines e a standard library inclui um servidor HTTP completo. Empresas como Uber, Twitch e Mercado Livre usam Go para suas APIs.

Qual a diferença entre criar API REST com Go e com Gin?

Com a standard library você não adiciona dependências e usa os recursos nativos do Go 1.22+ (roteamento por método, PathValue, http.Server com timeouts). Com Gin você ganha binding de JSON, validação por tags, middlewares prontos e sintaxe mais concisa, ao custo de uma dependência externa. Para APIs pequenas e medianas, a standard library basta.