Neste quinto e último artigo da série “Golang para Iniciantes”, vamos aplicar tudo o que aprendemos para construir uma API REST completa do zero. Você criará um servidor web funcional com rotas, handlers, JSON, persistência em memória e testes.

Se você está chegando agora, recomendamos revisar os artigos anteriores:

📖 ← Série Completa: Golang para Iniciantes

O Que Vamos Construir

Vamos criar uma API de Gerenciamento de Tarefas (Todo API) com as seguintes funcionalidades:

  • ✅ Criar tarefa (POST /tarefas)
  • ✅ Listar tarefas (GET /tarefas)
  • ✅ Buscar tarefa por ID (GET /tarefas/{id})
  • ✅ Atualizar tarefa (PUT /tarefas/{id})
  • ✅ Deletar tarefa (DELETE /tarefas/{id})
  • ✅ Marcar como concluída (PATCH /tarefas/{id}/concluir)

Estrutura do Projeto

todo-api/
├── go.mod
├── main.go
├── handlers/
│   └── tarefas.go
├── models/
│   └── tarefa.go
├── storage/
│   └── memory.go
└── go.sum

Configuração Inicial

1. Criar o Módulo

mkdir todo-api
cd todo-api
go mod init todo-api

2. Instalar Dependências

Vamos usar o Chi Router — leve, idiomático e rápido:

go get -u github.com/go-chi/chi/v5
go get -u github.com/go-chi/chi/v5/middleware

Modelos (models/tarefa.go)

Definimos a estrutura de dados:

package models

import (
    "encoding/json"
    "time"
)

// Tarefa representa uma tarefa no sistema
type Tarefa struct {
    ID          int       `json:"id"`
    Titulo      string    `json:"titulo"`
    Descricao   string    `json:"descricao"`
    Concluida   bool      `json:"concluida"`
    CriadaEm    time.Time `json:"criada_em"`
    AtualizadaEm time.Time `json:"atualizada_em"`
}

// CreateTarefaRequest representa o payload para criar tarefa
type CreateTarefaRequest struct {
    Titulo    string `json:"titulo"`
    Descricao string `json:"descricao"`
}

// Validate valida os dados da requisição
func (r CreateTarefaRequest) Validate() error {
    if r.Titulo == "" {
        return fmt.Errorf("título é obrigatório")
    }
    if len(r.Titulo) > 100 {
        return fmt.Errorf("título deve ter no máximo 100 caracteres")
    }
    return nil
}

// UpdateTarefaRequest representa o payload para atualizar tarefa
type UpdateTarefaRequest struct {
    Titulo    string `json:"titulo,omitempty"`
    Descricao string `json:"descricao,omitempty"`
    Concluida *bool  `json:"concluida,omitempty"`
}

Adicione o import:

import "fmt"

Armazenamento em Memória (storage/memory.go)

package storage

import (
    "errors"
    "sync"
    "time"
    "todo-api/models"
)

var (
    ErrTarefaNotFound = errors.New("tarefa não encontrada")
)

// MemoryStorage implementa armazenamento em memória
type MemoryStorage struct {
    mu       sync.RWMutex
    tarefas  map[int]models.Tarefa
    nextID   int
}

// NewMemoryStorage cria uma nova instância
func NewMemoryStorage() *MemoryStorage {
    return &MemoryStorage{
        tarefas: make(map[int]models.Tarefa),
        nextID:  1,
    }
}

// Create adiciona uma nova tarefa
func (s *MemoryStorage) Create(tarefa models.Tarefa) models.Tarefa {
    s.mu.Lock()
    defer s.mu.Unlock()

    tarefa.ID = s.nextID
    tarefa.CriadaEm = time.Now()
    tarefa.AtualizadaEm = time.Now()
    s.tarefas[tarefa.ID] = tarefa
    s.nextID++

    return tarefa
}

// GetByID busca uma tarefa pelo ID
func (s *MemoryStorage) GetByID(id int) (models.Tarefa, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    tarefa, ok := s.tarefas[id]
    if !ok {
        return models.Tarefa{}, ErrTarefaNotFound
    }
    return tarefa, nil
}

// GetAll retorna todas as tarefas
func (s *MemoryStorage) GetAll() []models.Tarefa {
    s.mu.RLock()
    defer s.mu.RUnlock()

    tarefas := make([]models.Tarefa, 0, len(s.tarefas))
    for _, t := range s.tarefas {
        tarefas = append(tarefas, t)
    }
    return tarefas
}

// Update atualiza uma tarefa existente
func (s *MemoryStorage) Update(id int, updates models.UpdateTarefaRequest) (models.Tarefa, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    tarefa, ok := s.tarefas[id]
    if !ok {
        return models.Tarefa{}, ErrTarefaNotFound
    }

    if updates.Titulo != "" {
        tarefa.Titulo = updates.Titulo
    }
    if updates.Descricao != "" {
        tarefa.Descricao = updates.Descricao
    }
    if updates.Concluida != nil {
        tarefa.Concluida = *updates.Concluida
    }

    tarefa.AtualizadaEm = time.Now()
    s.tarefas[id] = tarefa
    return tarefa, nil
}

// Delete remove uma tarefa
func (s *MemoryStorage) Delete(id int) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.tarefas[id]; !ok {
        return ErrTarefaNotFound
    }
    delete(s.tarefas, id)
    return nil
}

Handlers (handlers/tarefas.go)

package handlers

import (
    "encoding/json"
    "net/http"
    "strconv"
    "todo-api/models"
    "todo-api/storage"

    "github.com/go-chi/chi/v5"
)

// TarefaHandler gerencia as requisições de tarefas
type TarefaHandler struct {
    storage *storage.MemoryStorage
}

// NewTarefaHandler cria um novo handler
func NewTarefaHandler(s *storage.MemoryStorage) *TarefaHandler {
    return &TarefaHandler{storage: s}
}

// Create cria uma nova tarefa
func (h *TarefaHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req models.CreateTarefaRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondWithError(w, http.StatusBadRequest, "JSON inválido")
        return
    }

    if err := req.Validate(); err != nil {
        respondWithError(w, http.StatusBadRequest, err.Error())
        return
    }

    tarefa := models.Tarefa{
        Titulo:    req.Titulo,
        Descricao: req.Descricao,
    }

    created := h.storage.Create(tarefa)
    respondWithJSON(w, http.StatusCreated, created)
}

// GetAll lista todas as tarefas
func (h *TarefaHandler) GetAll(w http.ResponseWriter, r *http.Request) {
    tarefas := h.storage.GetAll()
    respondWithJSON(w, http.StatusOK, tarefas)
}

// GetByID busca uma tarefa específica
func (h *TarefaHandler) GetByID(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(chi.URLParam(r, "id"))
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "ID inválido")
        return
    }

    tarefa, err := h.storage.GetByID(id)
    if err != nil {
        respondWithError(w, http.StatusNotFound, "Tarefa não encontrada")
        return
    }

    respondWithJSON(w, http.StatusOK, tarefa)
}

// Update atualiza uma tarefa
func (h *TarefaHandler) Update(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(chi.URLParam(r, "id"))
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "ID inválido")
        return
    }

    var req models.UpdateTarefaRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondWithError(w, http.StatusBadRequest, "JSON inválido")
        return
    }

    tarefa, err := h.storage.Update(id, req)
    if err != nil {
        respondWithError(w, http.StatusNotFound, "Tarefa não encontrada")
        return
    }

    respondWithJSON(w, http.StatusOK, tarefa)
}

// Delete remove uma tarefa
func (h *TarefaHandler) Delete(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(chi.URLParam(r, "id"))
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "ID inválido")
        return
    }

    if err := h.storage.Delete(id); err != nil {
        respondWithError(w, http.StatusNotFound, "Tarefa não encontrada")
        return
    }

    respondWithJSON(w, http.StatusOK, map[string]string{
        "message": "Tarefa deletada com sucesso",
    })
}

// Concluir marca uma tarefa como concluída
func (h *TarefaHandler) Concluir(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(chi.URLParam(r, "id"))
    if err != nil {
        respondWithError(w, http.StatusBadRequest, "ID inválido")
        return
    }

    concluida := true
    tarefa, err := h.storage.Update(id, models.UpdateTarefaRequest{
        Concluida: &concluida,
    })
    if err != nil {
        respondWithError(w, http.StatusNotFound, "Tarefa não encontrada")
        return
    }

    respondWithJSON(w, http.StatusOK, tarefa)
}

// Helpers

func respondWithJSON(w http.ResponseWriter, status int, payload interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(payload)
}

func respondWithError(w http.ResponseWriter, status int, message string) {
    respondWithJSON(w, status, map[string]string{"error": message})
}

Ponto de Entrada (main.go)

package main

import (
    "fmt"
    "net/http"
    "todo-api/handlers"
    "todo-api/storage"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    // Inicializa armazenamento
    db := storage.NewMemoryStorage()

    // Cria handler
    tarefaHandler := handlers.NewTarefaHandler(db)

    // Configura router
    r := chi.NewRouter()

    // Middlewares
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)

    // Rotas
    r.Route("/tarefas", func(r chi.Router) {
        r.Get("/", tarefaHandler.GetAll)
        r.Post("/", tarefaHandler.Create)
        r.Get("/{id}", tarefaHandler.GetByID)
        r.Put("/{id}", tarefaHandler.Update)
        r.Delete("/{id}", tarefaHandler.Delete)
        r.Patch("/{id}/concluir", tarefaHandler.Concluir)
    })

    // Health check
    r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })

    // Inicia servidor
    port := ":8080"
    fmt.Printf("Servidor rodando em http://localhost%s\n", port)
    if err := http.ListenAndServe(port, r); err != nil {
        fmt.Printf("Erro ao iniciar servidor: %v\n", err)
    }
}

Testando a API

1. Iniciar o Servidor

go run main.go

2. Testar com curl

# Criar tarefa
curl -X POST http://localhost:8080/tarefas \
  -H "Content-Type: application/json" \
  -d '{
    "titulo": "Aprender Go",
    "descricao": "Completar o tutorial de Go"
  }'

# Listar tarefas
curl http://localhost:8080/tarefas

# Buscar tarefa específica
curl http://localhost:8080/tarefas/1

# Atualizar tarefa
curl -X PUT http://localhost:8080/tarefas/1 \
  -H "Content-Type: application/json" \
  -d '{
    "titulo": "Aprender Go Avançado",
    "descricao": "Dominar concorrência em Go"
  }'

# Marcar como concluída
curl -X PATCH http://localhost:8080/tarefas/1/concluir

# Deletar tarefa
curl -X DELETE http://localhost:8080/tarefas/1

# Health check
curl http://localhost:8080/health

Melhorias e Próximos Passos

1. Adicionar Banco de Dados

Substitua o MemoryStorage por PostgreSQL ou MongoDB:

// storage/postgres.go
package storage

import (
    "database/sql"
    _ "github.com/lib/pq"
)

type PostgresStorage struct {
    db *sql.DB
}

func NewPostgresStorage(connStr string) (*PostgresStorage, error) {
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        return nil, err
    }
    return &PostgresStorage{db: db}, nil
}

2. Adicionar Testes

// handlers/tarefas_test.go
package handlers

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
    "todo-api/models"
    "todo-api/storage"
)

func TestCreateTarefa(t *testing.T) {
    db := storage.NewMemoryStorage()
    handler := NewTarefaHandler(db)

    reqBody := `{"titulo":"Teste","descricao":"Descrição"}`
    req := httptest.NewRequest(http.MethodPost, "/tarefas", bytes.NewBufferString(reqBody))
    req.Header.Set("Content-Type", "application/json")
    
    rr := httptest.NewRecorder()
    handler.Create(rr, req)

    if rr.Code != http.StatusCreated {
        t.Errorf("Esperado %d, obtido %d", http.StatusCreated, rr.Code)
    }

    var response models.Tarefa
    json.Unmarshal(rr.Body.Bytes(), &response)
    
    if response.Titulo != "Teste" {
        t.Errorf("Título incorreto: %s", response.Titulo)
    }
}

3. Dockerizar

# Dockerfile
FROM golang:1.23-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o todo-api .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/todo-api .
EXPOSE 8080
CMD ["./todo-api"]

4. Adicionar Autenticação

// middleware/auth.go
package middleware

import (
    "net/http"
    "strings"
)

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" || !strings.HasPrefix(token, "Bearer ") {
            http.Error(w, "Não autorizado", http.StatusUnauthorized)
            return
        }
        // Validar token...
        next.ServeHTTP(w, r)
    })
}

Resumo do Que Você Aprendeu

Nesta série completa, você aprendeu:

  1. Artigo 1: Instalação, configuração e primeiro programa
  2. Artigo 2: Variáveis, tipos, funções, structs e métodos
  3. Artigo 3: Controle de fluxo (if, switch, loops)
  4. Artigo 4: Concorrência com goroutines e channels
  5. Artigo 5: API REST completa

Você agora tem uma base sólida em Go e pode construir aplicações web reais!

Próximos Passos

Continue Seu Aprendizado

Projetos Sugeridos

  1. API de Blog — CRUD completo com autenticação JWT
  2. WebSocket Chat — Chat em tempo real
  3. CLI Tool — Ferramenta de linha de comando
  4. Microserviço — Com gRPC ou REST
  5. Pipeline de Dados — Processamento concorrente

Comunidade


Parabéns por completar a série “Golang para Iniciantes”! 🎉

Você está pronto para construir aplicações Go profissionais. Boa codificação!


Veja a série completa: Golang para Iniciantes


Última atualização: 09 de fevereiro de 2026
Versão do Go: 1.23