M01·06Mini Projeto: API REST de Tarefas

CAPÍTULO 06

Mini Projeto: API REST de Tarefas

Construa uma API REST funcional com 4 endpoints usando só a biblioteca padrão do Go.

Por Thiago Souza14 min de leituraAtualizado em 2026-05

Vamos construir uma API REST funcional para gerenciar uma lista de tarefas (TO-DO). Esse é o "hello world" de backend.

O que vamos construir

Uma API com 4 endpoints:

MétodoRotaO que faz
GET/tarefasLista todas as tarefas
GET/tarefas/{id}Busca uma tarefa específica
POST/tarefasCria uma nova tarefa
DELETE/tarefas/{id}Remove uma tarefa

Estrutura do projeto

api-tarefas/
├── go.mod
└── main.go

Para começar, num terminal:

bash
mkdir api-tarefas && cd api-tarefas
go mod init github.com/seu-usuario/api-tarefas
O que go mod init faz? Cria o go.mod, que é como o package.json do Node ou o pom.xml do Maven. Ele rastreia dependências e a versão do Go.

O código completo (main.go)

go
package main
 
import (
    "encoding/json"  // serialização JSON
    "fmt"
    "log"
    "net/http"        // servidor HTTP da stdlib
    "strconv"
    "sync"            // para sincronização (mutex)
)
 
// ==========================================================
// MODELO: como uma tarefa se parece
// ==========================================================
type Task struct {
    // As tags `json:"..."` definem como o campo aparece em JSON
    ID    int    `json:"id"`
    Title string `json:"titulo"`
    Done  bool   `json:"concluida"`
}
 
// ==========================================================
// REPOSITÓRIO: onde guardamos os dados
// (Em produção seria um banco de dados; aqui usamos memória)
// ==========================================================
type Repository struct {
    mu       sync.RWMutex   // protege contra acessos concorrentes
    tasks    map[int]Task
    nextID   int
}
 
func NewRepository() *Repository {
    return &Repository{
        tasks:  make(map[int]Task),
        nextID: 1,
    }
}
 
// Listar retorna todas as tarefas (cópia segura).
// RLock = lock de LEITURA: vários podem ler ao mesmo tempo.
func (r *Repository) Listar() []Task {
    r.mu.RLock()
    defer r.mu.RUnlock()
 
    lista := make([]Task, 0, len(r.tasks))
    for _, t := range r.tasks {
        lista = append(lista, t)
    }
    return lista
}
 
// Buscar pega uma tarefa pelo ID.
// Retorna (tarefa, true) se achou, (vazia, false) se não.
func (r *Repository) Buscar(id int) (Task, bool) {
    r.mu.RLock()
    defer r.mu.RUnlock()
 
    t, ok := r.tasks[id]
    return t, ok
}
 
// Criar adiciona uma nova tarefa e retorna ela com ID.
// Lock = lock de ESCRITA: só um por vez.
func (r *Repository) Criar(title string) Task {
    r.mu.Lock()
    defer r.mu.Unlock()
 
    t := Task{
        ID:    r.nextID,
        Title: title,
        Done:  false,
    }
    r.tasks[t.ID] = t
    r.nextID++
    return t
}
 
// Remover apaga pelo ID. Retorna true se removeu, false se não existia.
func (r *Repository) Remover(id int) bool {
    r.mu.Lock()
    defer r.mu.Unlock()
 
    if _, exists := r.tasks[id]; !exists {
        return false
    }
    delete(r.tasks, id)
    return true
}
 
// ==========================================================
// HANDLERS: as funções que recebem requisições HTTP
// ==========================================================
type Handler struct {
    repo *Repository
}
 
// responderJSON é uma função utilitária que padroniza respostas JSON
func responderJSON(w http.ResponseWriter, status int, dados any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    if dados != nil {
        if err := json.NewEncoder(w).Encode(dados); err != nil {
            log.Println("erro ao codificar JSON:", err)
        }
    }
}
 
// responderErro padroniza mensagens de erro
func responderErro(w http.ResponseWriter, status int, mensagem string) {
    responderJSON(w, status, map[string]string{"erro": mensagem})
}
 
// handleTasks roteia requisições para /tarefas (sem ID)
func (h *Handler) handleTasks(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // GET /tarefas → lista todas
        responderJSON(w, http.StatusOK, h.repo.Listar())
 
    case http.MethodPost:
        // POST /tarefas → cria nova
        var entrada struct {
            Title string `json:"titulo"`
        }
 
        // Decodifica o JSON do body em entrada
        if err := json.NewDecoder(r.Body).Decode(&entrada); err != nil {
            responderErro(w, http.StatusBadRequest, "JSON inválido")
            return
        }
 
        if entrada.Title == "" {
            responderErro(w, http.StatusBadRequest, "título é obrigatório")
            return
        }
 
        nova := h.repo.Criar(entrada.Title)
        responderJSON(w, http.StatusCreated, nova)
 
    default:
        responderErro(w, http.StatusMethodNotAllowed, "método não permitido")
    }
}
 
// handleTaskByID lida com /tarefas/{id}
func (h *Handler) handleTaskByID(w http.ResponseWriter, r *http.Request) {
    // Extrai o ID da URL: /tarefas/42 → "42"
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        responderErro(w, http.StatusBadRequest, "ID inválido")
        return
    }
 
    switch r.Method {
    case http.MethodGet:
        t, ok := h.repo.Buscar(id)
        if !ok {
            responderErro(w, http.StatusNotFound, "tarefa não encontrada")
            return
        }
        responderJSON(w, http.StatusOK, t)
 
    case http.MethodDelete:
        if !h.repo.Remover(id) {
            responderErro(w, http.StatusNotFound, "tarefa não encontrada")
            return
        }
        responderJSON(w, http.StatusNoContent, nil)
 
    default:
        responderErro(w, http.StatusMethodNotAllowed, "método não permitido")
    }
}
 
// ==========================================================
// MAIN: monta tudo e sobe o servidor
// ==========================================================
func main() {
    repo := NewRepository()
    handler := &Handler{repo: repo}
 
    // O ServeMux do Go 1.22+ suporta padrões avançados como {id}
    mux := http.NewServeMux()
    mux.HandleFunc("/tarefas", handler.handleTasks)
    mux.HandleFunc("/tarefas/{id}", handler.handleTaskByID)
 
    porta := ":8080"
    fmt.Printf("Servidor rodando em http://localhost%s\n", porta)
 
    // ListenAndServe BLOQUEIA — só retorna se der erro fatal
    if err := http.ListenAndServe(porta, mux); err != nil {
        log.Fatal("servidor falhou:", err)
    }
}

Testando a API

Rode com:

bash
go run main.go

Em outro terminal:

bash
# Criar uma tarefa
curl -X POST http://localhost:8080/tarefas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Estudar Go"}'
 
# Listar tarefas
curl http://localhost:8080/tarefas
 
# Buscar tarefa específica
curl http://localhost:8080/tarefas/1
 
# Remover
curl -X DELETE http://localhost:8080/tarefas/1

Por que tomamos cada decisão?

DecisãoMotivo
Sem framework (Gin, Echo)A net/http da stdlib é poderosíssima. Para aprender, melhor não esconder atrás de mágica.
Mutex no repositórioServidores HTTP em Go atendem várias requisições concorrentemente. Sem lock, dois clientes podem corromper o map.
RWMutex em vez de MutexPermite várias leituras simultâneas, só bloqueando escritas. Ganho enorme em APIs read-heavy.
Camadas (handler/repositório)Separar responsabilidades facilita testes e troca de banco depois.
Funções utilitárias responderJSONEvita repetição de w.Header().Set(...) em todo lugar.
json.NewDecoder(r.Body)Mais eficiente que ler tudo e dar Unmarshal — funciona como stream.
Status HTTP corretos201 Created para criação, 204 No Content para deletar, 404 para não encontrado. RESTful de verdade.