Desenvolvimento de REST API
Desenvolvimento de REST API
Analogia do Mundo Real
Imagine que você administra um restaurante. Os clientes (clientes) chegam à recepção, e o garçom (roteador) os direciona para diferentes janelas com base em suas necessidades:
- Ver cardápio → Janela de consulta da recepção (GET)
- Fazer pedido → Janela de pedidos (POST)
- Modificar pedido → Janela de modificação de pedido (PUT)
- Cancelar pedido → Janela de cancelamento de pedido (DELETE)
A cozinha (servidor) processa a solicitação e entrega o prato preparado (resposta) de volta ao cliente através do garçom. Enquanto isso, o segurança (middleware) verifica a identidade antes dos clientes entrarem, registra horários de visita e pode lidar com emergências.
Uma REST API é um fluxo de trabalho de serviço padronizado como este — os clientes usam "verbos" uniformes (métodos HTTP) e "endereços" (URLs) para se comunicar com o servidor, e o servidor retorna dados estruturados (JSON).
Requisitos do Projeto
Desenvolveremos uma API REST de Todo suportando os seguintes recursos:
| Operação | Método | Caminho | Descrição |
|---|---|---|---|
| Obter todas as tarefas | GET |
/api/todos |
Retorna a lista de tarefas |
| Obter tarefa única | GET |
/api/todos/{id} |
Retorna detalhes por ID |
| Criar tarefa | POST |
/api/todos |
Adiciona uma nova tarefa |
| Atualizar tarefa | PUT |
/api/todos/{id} |
Modifica a tarefa especificada |
| Excluir tarefa | DELETE |
/api/todos/{id} |
Exclui a tarefa especificada |
Requisitos adicionais:
- Usar armazenamento em memória (map + mutex)
- Implementar cadeia de middleware: logging, autenticação, recuperação de panic
- Requisições e respostas usam formato JSON
Design do Sistema
Requisição do Cliente
│
▼
┌──────────────────────────────┐
│ Cadeia de Middleware │
│ Recovery → Auth → Logging │
│ ↓ ↓ ↓ │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Roteador │
│ GET /api/todos │
│ GET /api/todos/{id} │
│ POST /api/todos │
│ PUT /api/todos/{id} │
│ DELETE /api/todos/{id} │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Camada de Armazenamento em Memória │
│ map[string]Todo + sync.RWMutex │
└──────────────────────────────┘
Exemplo 1: Código Completo
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
// Estrutura do item Todo
type Todo struct {
ID string `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// TodoStore armazenamento em memória com trava de leitura-escrita para segurança de concorrência
type TodoStore struct {
mu sync.RWMutex
todos map[string]Todo
}
// NewTodoStore cria uma nova instância de armazenamento
func NewTodoStore() *TodoStore {
return &TodoStore{
todos: make(map[string]Todo),
}
}
// GetAll retorna todos os itens de tarefa
func (s *TodoStore) GetAll() []Todo {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]Todo, 0, len(s.todos))
for _, t := range s.todos {
list = append(list, t)
}
return list
}
// GetByID retorna um único item de tarefa por ID
func (s *TodoStore) GetByID(id string) (Todo, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
t, ok := s.todos[id]
return t, ok
}
// Create cria um novo item de tarefa
func (s *TodoStore) Create(title string) Todo {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().Format(time.RFC3339)
t := Todo{
ID: uuid.New().String(),
Title: title,
Completed: false,
CreatedAt: now,
UpdatedAt: now,
}
s.todos[t.ID] = t
return t
}
// Update atualiza o item de tarefa especificado
func (s *TodoStore) Update(id string, title string, completed bool) (Todo, bool) {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.todos[id]
if !ok {
return Todo{}, false
}
t.Title = title
t.Completed = completed
t.UpdatedAt = time.Now().Format(time.RFC3339)
s.todos[id] = t
return t, true
}
// Delete exclui o item de tarefa especificado
func (s *TodoStore) Delete(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.todos[id]; !ok {
return false
}
delete(s.todos, id)
return true
}
// --- Estruturas de Resposta Comuns ---
// APIResponse formato de resposta API unificado
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// jsonResponse envia uma resposta JSON
func jsonResponse(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// --- Middleware ---
// Definição do tipo Middleware
type Middleware func(http.Handler) http.Handler
// RecoveryMiddleware middleware de recuperação de panic
// Captura panics nas funções handler, retorna erro 500 em vez de travar o processo
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
jsonResponse(w, http.StatusInternalServerError, APIResponse{
Success: false,
Error: "Erro interno do servidor",
})
}
}()
next.ServeHTTP(w, r)
})
}
// LoggingMiddleware middleware de logging de requisições
// Registra o método, caminho e duração de cada requisição
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("[LOG] %s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
// AuthMiddleware middleware simples de autenticação
// Verifica se o cabeçalho da requisição contém uma API Key válida
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("X-API-Key")
if apiKey != "secret-key-123" {
jsonResponse(w, http.StatusUnauthorized, APIResponse{
Success: false,
Error: "Chave de API inválida",
})
return
}
next.ServeHTTP(w, r)
})
}
// Chain combina múltiplos middlewares em uma cadeia
// A ordem dos parâmetros é a ordem de execução: Chain(A, B, C) → A(B(C(handler)))
func Chain(middlewares ...Middleware) Middleware {
return func(next http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
next = middlewares[i](next)
}
return next
}
}
// --- Roteador e Handlers ---
// Router roteador personalizado
type Router struct {
routes map[string]map[string]http.HandlerFunc // método -> caminho -> handler
middleware Middleware
}
// NewRouter cria um roteador
func NewRouter() *Router {
return &Router{
routes: make(map[string]map[string]http.HandlerFunc),
}
}
// Use registra middleware global
func (rt *Router) Use(m Middleware) {
rt.middleware = m
}
// Handle registra uma rota: método + caminho + handler
func (rt *Router) Handle(method, path string, handler http.HandlerFunc) {
if _, ok := rt.routes[method]; !ok {
rt.routes[method] = make(map[string]http.HandlerFunc)
}
rt.routes[method][path] = handler
}
// ServeHTTP implementa a interface http.Handler, completando a correspondência de rotas
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
methodRoutes, ok := rt.routes[r.Method]
if !ok {
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{
Success: false,
Error: "Método não permitido",
})
return
}
// Correspondência exata
if handler, ok := methodRoutes[r.URL.Path]; ok {
handler(w, r)
return
}
// Correspondência de prefixo: tentar extrair parâmetros de caminho (ex: /api/todos/{id})
for pattern, handler := range methodRoutes {
if strings.HasSuffix(pattern, "/{id}") {
prefix := strings.TrimSuffix(pattern, "/{id}")
if strings.HasPrefix(r.URL.Path, prefix+"/") {
// Colocar {id} no cabeçalho da requisição para o handler ler
id := strings.TrimPrefix(r.URL.Path, prefix+"/")
r.Header.Set("X-Resource-ID", id)
handler(w, r)
return
}
}
}
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "Recurso não encontrado",
})
}
// --- Funções Handler ---
// handleGetTodos retorna todos os itens de tarefa
func handleGetTodos(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
todos := store.GetAll()
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todos,
})
}
}
// handleGetTodo retorna um único item de tarefa
func handleGetTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
todo, ok := store.GetByID(id)
if !ok {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "Item de tarefa não encontrado",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todo,
})
}
}
// CreateTodoRequest corpo da requisição para criar uma tarefa
type CreateTodoRequest struct {
Title string `json:"title"`
}
// handleCreateTodo cria um item de tarefa
func handleCreateTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req CreateTodoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "Corpo da requisição inválido",
})
return
}
if strings.TrimSpace(req.Title) == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "Título não pode ser vazio",
})
return
}
todo := store.Create(req.Title)
jsonResponse(w, http.StatusCreated, APIResponse{
Success: true,
Data: todo,
})
}
}
// UpdateTodoRequest corpo da requisição para atualizar uma tarefa
type UpdateTodoRequest struct {
Title string `json:"title"`
Completed bool `json:"completed"`
}
// handleUpdateTodo atualiza um item de tarefa
func handleUpdateTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
var req UpdateTodoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "Corpo da requisição inválido",
})
return
}
if strings.TrimSpace(req.Title) == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Error: "Título não pode ser vazio",
})
return
}
todo, ok := store.Update(id, req.Title, req.Completed)
if !ok {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "Item de tarefa não encontrado",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: todo,
})
}
}
// handleDeleteTodo exclui um item de tarefa
func handleDeleteTodo(store *TodoStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Resource-ID")
if !store.Delete(id) {
jsonResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Error: "Item de tarefa não encontrado",
})
return
}
jsonResponse(w, http.StatusOK, APIResponse{
Success: true,
Data: "Excluído com sucesso",
})
}
}
func main() {
// Inicializar armazenamento
store := NewTodoStore()
// Criar roteador
router := NewRouter()
// Registrar cadeia de middleware: Recovery → Auth → Logging
router.Use(Chain(RecoveryMiddleware, AuthMiddleware, LoggingMiddleware))
// Registrar rotas
router.Handle("GET", "/api/todos", handleGetTodos(store))
router.Handle("GET", "/api/todos/{id}", handleGetTodo(store))
router.Handle("POST", "/api/todos", handleCreateTodo(store))
router.Handle("PUT", "/api/todos/{id}", handleUpdateTodo(store))
router.Handle("DELETE", "/api/todos/{id}", handleDeleteTodo(store))
// Aplicar middleware e iniciar servidor
addr := ":8080"
log.Printf("Servidor REST API iniciado em http://localhost%s", addr)
log.Fatal(http.ListenAndServe(addr, router.middleware(router)))
}
Executando e Testando
Antes de iniciar o serviço, inicialize o módulo e instale as dependências:
go mod init todo-api
go get github.com/google/uuid
go run main.go
Use curl para testar cada endpoint:
# Criar um item de tarefa
curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-H "X-API-Key: secret-key-123" \
-d '{"title": "Aprender Go"}'
# Obter todas as tarefas
curl http://localhost:8080/api/todos \
-H "X-API-Key: secret-key-123"
# Obter uma única tarefa (substitua {id} pelo ID real retornado)
curl http://localhost:8080/api/todos/{id} \
-H "X-API-Key: secret-key-123"
# Atualizar uma tarefa
curl -X PUT http://localhost:8080/api/todos/{id} \
-H "Content-Type: application/json" \
-H "X-API-Key: secret-key-123" \
-d '{"title": "Aprender Go (concluído)", "completed": true}'
# Excluir uma tarefa
curl -X DELETE http://localhost:8080/api/todos/{id} \
-H "X-API-Key: secret-key-123"
# Testar requisição não autenticada (deve retornar 401)
curl http://localhost:8080/api/todos
Exemplo de respostas esperadas:
# Criação bem-sucedida
{"success":true,"data":{"id":"a1b2c3d4-...","title":"Aprender Go","completed":false,"created_at":"2026-06-27T10:00:00+08:00","updated_at":"2026-06-27T10:00:00+08:00"}}
# Não autenticado
{"success":false,"error":"Chave de API inválida"}
Análise do Código
1. Modelo de Dados e Camada de Armazenamento
A estrutura Todo define a estrutura de dados para itens de tarefa, usando tags JSON para controlar nomes de campos serializados. TodoStore usa sync.RWMutex para proteger o map — operações de leitura usam trava de leitura (RLock), operações de escrita usam trava de escrita (Lock), garantindo segurança de concorrência.
2. Padrão de Middleware
Cada middleware é do tipo func(http.Handler) http.Handler, envolvendo o próximo handler através do padrão decorador:
RecoveryMiddleware(
AuthMiddleware(
LoggingMiddleware(
handler final,
),
),
)
- RecoveryMiddleware: Usa
defer + recoverpara capturar panics - AuthMiddleware: Verifica a API Key no cabeçalho da requisição
- LoggingMiddleware: Registra método, caminho e duração da requisição
A função Chain combina múltiplos middlewares na ordem dos parâmetros em uma cadeia.
3. Roteador Personalizado
Como o http.ServeMux da biblioteca padrão não suportava parâmetros de caminho antes do Go 1.22, implementamos um roteador simples:
- Usa
map[method][path]aninhado para armazenar a tabela de roteamento - Correspondência exata tem prioridade; em caso de falha, tenta correspondência de prefixo para extrair
{id} - Parâmetros de caminho são passados para handlers através do cabeçalho de requisição
X-Resource-ID
4. Closures de Handler
Funções como handleGetTodos(store) retornam uma closure que captura a referência store. Isso permite que os handlers acessem a camada de armazenamento compartilhada sem variáveis globais.
5. Formato de Resposta Unificado
Todos os endpoints retornam a mesma estrutura JSON {success, data, error}, para que os clientes precisem analisar apenas uma vez para determinar se a requisição foi bem-sucedida.
❓ Perguntas Frequentes
P1: Por que usar sync.RWMutex em vez de sync.Mutex?
Mutex permite que apenas uma goroutine acesse por vez, enquanto RWMutex permite que múltiplas operações de leitura executem concorrentemente — apenas operações de escrita travam exclusivamente. Para cenários de "leitura intensa, escrita leve" (como consultas de API muito superiores a modificações), RWMutex pode melhorar significativamente o desempenho concorrente.
P2: Este roteador deve ser usado em produção?
Não recomendado. Este roteador é uma implementação simplificada para fins educacionais, faltando suporte para correspondência de regex, curingas, auto-detecção de método e outros recursos avançados. Para produção, use bibliotecas de roteamento maduras como chi, gorilla/mux, ou o roteamento aprimorado embutido do Go 1.22+.
P3: Os dados do armazenamento em memória são perdidos após reinicialização. Como resolver?
Esta lição usa armazenamento em memória para focar na parte da REST API. Em projetos reais, você deve persistir dados em um banco de dados (como SQLite, PostgreSQL, MongoDB). A próxima lição cobrirá como operar bancos de dados com Go.
P4: A ordem de execução do middleware importa?
Sim. Recovery vai mais externo para garantir que capture todos os panics internos; Auth vai antes de Logging para que requisições não autenticadas não gerem logs de negócio (embora você possa fazer o oposto para logar todas as requisições incluindo não autenticadas). Ajuste flexivelmente com base nas necessidades do negócio.
📖 Resumo
Nesta lição aplicamos de forma abrangente múltiplos pontos de conhecimento aprendidos anteriormente para construir uma REST API completa do zero:
- Usamos
net/httppara implementar um servidor HTTP e roteador personalizado - Usamos
encoding/jsonpara serialização e desserialização JSON - Usamos
sync.RWMutexpara armazenamento em memória seguro para concorrência - Usamos o padrão de cadeia de middleware para preocupações transversais (logging, autenticação, recuperação)
- Usamos closures para injeção de dependência, permitindo que handlers acessem recursos compartilhados
- Projetamos caminhos de API e métodos HTTP seguindo as convenções RESTful
Esses padrões são a base do desenvolvimento web em Go. Após dominá-los, você pode facilmente estender para integração com banco de dados, autenticação JWT, limitação de taxa de API e outros cenários complexos.
📝 Exercícios
Exercício 1: Adicionar Suporte a Paginação
Modifique o endpoint GET /api/todos para suportar parâmetros de consulta ?page=1&size=10 para resultados paginados. Dica: use r.URL.Query() para ler parâmetros e fatiar os resultados de GetAll.
Exercício 2: Adicionar Middleware CORS
Escreva CORSMiddleware que adicione o seguinte aos cabeçalhos de resposta:
Access-Control-Allow-Origin: *Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, X-API-Key
Trate requisições preflight OPTIONS retornando 204 diretamente. Adicione-o à cadeia de middleware.
Exercício 3: Implementar Funcionalidade de Busca
Adicione uma nova rota GET /api/todos/search?q=keyword que realize correspondência difusa de título no armazenamento em memória (usando strings.Contains) e retorne itens de tarefa correspondentes.
Próxima Lição: Operações com Banco de Dados



