Go: API REST na prática

Uma API REST vai além do simples CRUD — o projeto dos recursos, a seleção de códigos de status, a formatação consistente de erros e as cadeias de middleware: cada detalhe determina a qualidade da API em ambiente de produção.

Quando sua API precisa ser chamada tanto por aplicativos front-end quanto por serviços de terceiros, um formato padronizado de erros, códigos de status adequados e um gerenciamento claro de versões não são mais apenas “recursos desejáveis” — eles são “requisitos indispensáveis”.

1. Você aprenderá


2. Uma história real contada por um colaborador da área de front-end

(1) Problema: os formatos dos erros de API variam de acordo com a interface, causando falhas no front-end

As equipes de back-end e front-end da Alice estão colaborando em um projeto de comércio eletrônico:

“Meus colegas de front-end disseram: ‘Os formatos de erro de cada uma das suas APIs são diferentes. A API de lista de usuários retorna {"error":"not found"}, a API de pedidos retorna {"message":"Order not found","code":404} e a API de produtos simplesmente retorna uma página de erro 500. Tenho que escrever um código de tratamento de erros diferente para cada API!’”

GO
// Bad code: inconsistent error formats
// GET /users/1 → {"error":"not found"}            ← Format A
// GET /orders/1 → {"message":"Order not found","code":404}  ← Format B
// GET /products → <html>500 Internal Error</html>            ← Format C

(2) Solução do Go: Respostas padronizadas a erros

GO
// Unified error format
type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Detail  string `json:"detail,omitempty"`
}

func (e *APIError) Error() string {
    return e.Message
}

// Factory functions
func NotFound(msg string) *APIError {
    return &APIError{Code: 404, Message: "not_found", Detail: msg}
}

func BadRequest(msg string) *APIError {
    return &APIError{Code: 400, Message: "bad_request", Detail: msg}
}

func InternalError(msg string) *APIError {
    return &APIError{Code: 500, Message: "internal_error", Detail: msg}
}

(3) Receita: antes e depois da unificação

Dimensão Inconsistente Formato unificado
Processamento front-end Escrever lógicas diferentes para cada API if (resp.error) handleError(resp)
Custo da documentação Documentação separada para cada API Formato de descrição em uma frase
Geração de SDK Não pode ser automatizada Gerar cliente diretamente via OpenAPI
Custos de depuração Verificar o formato específico a cada vez Nomes de campos padronizados

3. Princípios de projeto RESTful

(1) Projeto de recursos

GO
// Good RESTful URL design:
// Resource (noun) + Verb (HTTP method)

// Single resource
GET    /users          → List (collection)
POST   /users          → Create
GET    /users/{id}     → View single
PUT    /users/{id}     → Full update
PATCH  /users/{id}     → Partial update
DELETE /users/{id}     → Delete

// Sub-resource
GET    /users/{id}/orders      → User's order list
POST   /users/{id}/orders      → Create order for user
GET    /users/{id}/orders/{oid} → User's specific order

// Actions (use verbs for non-CRUD)
POST   /users/{id}/activate    → Activate user
POST   /orders/{id}/cancel     → Cancel order

(2) Seleção do código de status

GO
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

// Unified response
type Response struct {
    Data  interface{} `json:"data,omitempty"`
    Error *APIError   `json:"error,omitempty"`
    Meta  *Meta       `json:"meta,omitempty"`
}

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Detail  string `json:"detail,omitempty"`
}

type Meta struct {
    Total   int `json:"total"`
    Page    int `json:"page"`
    PerPage int `json:"per_page"`
}

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

func main() {
    fmt.Println("Response types: Response, APIError, Meta")
    fmt.Println("Helper: writeJSON(w, status, data)")
}
Código de status Método Significado
200 OK GET Consulta bem-sucedida
201 Criado POST Criado com sucesso
204 Sem conteúdo EXCLUIR Excluído com sucesso
400 Solicitação inválida Parâmetros de solicitação inválidos
401 Acesso não autorizado Acesso não autorizado
403 Proibido Sem permissão
404 Não encontrado O recurso não existe
409 Conflito POST/PUT Conflito de recurso (por exemplo, criação duplicada)
422 Não processável POST/PUT Erro semântico no corpo da solicitação
429 Demais Limite de solicitações
500 Interno Erro do servidor

4. Prática com a API REST

(1) ▶ Exemplo: API CRUD de usuário

⚙️ Pré-requisito: Execute go get github.com/mattn/go-sqlite3 (se estiver usando o SQLite)

GO 📖 Somente leitura
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "sync"
    "time"
)

// ---------- Model ----------

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UpdateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// ---------- Store ----------

type UserStore struct {
    mu     sync.RWMutex
    users  map[int]User
    nextID int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *UserStore) List() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]User, 0, len(s.users))
    for _, u := range s.users {
        result = append(result, u)
    }
    return result
}

func (s *UserStore) Create(req CreateUserRequest) User {
    s.mu.Lock()
    defer s.mu.Unlock()
    u := User{
        ID:        s.nextID,
        Name:      req.Name,
        Email:     req.Email,
        CreatedAt: time.Now(),
    }
    s.nextID++
    s.users[u.ID] = u
    return u
}

func (s *UserStore) Get(id int) (User, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    u, ok := s.users[id]
    return u, ok
}

func (s *UserStore) Update(id int, req UpdateUserRequest) (User, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    u, ok := s.users[id]
    if !ok {
        return User{}, false
    }
    u.Name = req.Name
    u.Email = req.Email
    s.users[id] = u
    return u, true
}

func (s *UserStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.users[id]
    if !ok {
        return false
    }
    Excluir(s.users, id)
    return true
}

// ---------- Handler ----------

type UserHandler struct {
    store *UserStore
}

func NewUserHandler(store *UserStore) *UserHandler {
    return &UserHandler{store: store}
}

func (h *UserHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/v1/users", h.ListUsers)
    mux.HandleFunc("POST /api/v1/users", h.CreateUser)
    mux.HandleFunc("GET /api/v1/users/{id}", h.GetUser)
    mux.HandleFunc("PUT /api/v1/users/{id}", h.UpdateUser)
    mux.HandleFunc("DELETE /api/v1/users/{id}", h.DeleteUser)
}

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

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]interface{}{
        "error": map[string]interface{}{
            "code":    status,
            "message": message,
        },
    })
}

func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
    users := h.store.List()
    writeJSON(w, http.StatusOK, map[string]interface{}{
        "data": users,
        "meta": map[string]int{"total": len(users)},
    })
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    // Parameter validation
    if req.Name == "" {
        writeError(w, http.StatusBadRequest, "name is required")
        return
    }
    if req.Email == "" {
        writeError(w, http.StatusBadRequest, "email is required")
        return
    }

    user := h.store.Create(req)
    writeJSON(w, http.StatusCreated, map[string]interface{}{
        "data": user,
    })
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid user ID")
        return
    }

    user, ok := h.store.Get(id)
    if !ok {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }

    writeJSON(w, http.StatusOK, map[string]interface{}{
        "data": user,
    })
}

func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid user ID")
        return
    }

    var req UpdateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    user, ok := h.store.Update(id, req)
    if !ok {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }

    writeJSON(w, http.StatusOK, map[string]interface{}{
        "data": user,
    })
}

func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid user ID")
        return
    }

    if !h.store.Delete(id) {
        writeError(w, http.StatusNotFound, "user not found")
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func main() {
    store := NewUserStore()
    handler := NewUserHandler(store)

    mux := http.NewServeMux()
    handler.Register(mux)

    log.Println("User API started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
192 linhas de lógica (limite de 40, somente leitura)

5. Validação de parâmetros

(1) ▶ Exemplo: Validação estruturada

GO 📖 Somente leitura
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "regexp"
    "strings"
)

// Validator
type Validator struct {
    errors []string
}

func (v *Validator) Required(field, value string) {
    if strings.TrimSpace(value) == "" {
        v.errors = append(v.errors, fmt.Sprintf("%s is required", field))
    }
}

func (v *Validator) MinLength(field, value string, min int) {
    if len(value) < min {
        v.errors = append(v.errors, fmt.Sprintf("%s must be at least %d characters", field, min))
    }
}

func (v *Validator) MaxLength(field, value string, max int) {
    if len(value) > max {
        v.errors = append(v.errors, fmt.Sprintf("%s must be at most %d characters", field, max))
    }
}

func (v *Validator) Email(field, value string) {
    pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
    matched, _ := regexp.MatchString(pattern, value)
    if !matched {
        v.errors = append(v.errors, fmt.Sprintf("%s is not a valid email", field))
    }
}

func (v *Validator) Valid() bool {
    return len(v.errors) == 0
}

func (v *Validator) Errors() []string {
    return v.errors
}

// ---------- Usage ----------

type SignupRequest struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
    Age      int    `json:"age"`
}

func validateSignup(req SignupRequest) *Validator {
    v := &Validator{}
    v.Required("name", req.Name)
    v.MinLength("name", req.Name, 2)
    v.MaxLength("name", req.Name, 50)
    v.Required("email", req.Email)
    v.Email("email", req.Email)
    v.Required("password", req.Password)
    v.MinLength("password", req.Password, 8)
    return v
}

func signupHandler(w http.ResponseWriter, r *http.Request) {
    var req SignupRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    if v := validateSignup(req); !v.Valid() {
        writeJSON(w, http.StatusUnprocessableEntity, map[string]interface{}{
            "error": map[string]interface{}{
                "code":    422,
                "message": "validation failed",
                "details": v.Errors(),
            },
        })
        return
    }

    writeJSON(w, http.StatusCreated, map[string]string{"status": "ok"})
}

func main() {
    v := validateSignup(SignupRequest{Name: "A", Email: "bad", Password: "123"})
    if !v.Valid() {
        fmt.Println("Validation errors:", v.Errors())
    }
}
80 linhas de lógica (limite de 40, somente leitura)

6. Integração da cadeia de middleware

(1) ▶ Exemplo: cadeia de middleware de API

GO 📖 Somente leitura
package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "strings"
    "time"
)

type contextKey string

const UserContextKey contextKey = "user"

// ---------- Middleware ----------

type Middleware func(http.Handler) http.Handler

func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

// Request logging
func RequestLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
        next.ServeHTTP(w, r)
        log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// Auth (simple Token)
func Auth(token string) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            auth := r.Header.Get("Authorization")
            if !strings.HasPrefix(auth, "Bearer ") {
                writeError(w, http.StatusUnauthorized, "missing or invalid token")
                return
            }
            if auth[7:] != token {
                writeError(w, http.StatusForbidden, "invalid token")
                return
            }
            // Inject user info into Context
            ctx := context.WithValue(r.Context(), UserContextKey, "admin")
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// Request timeout
func RequestTimeout(timeout time.Duration) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx, cancel := context.WithTimeout(r.Context(), timeout)
            defer cancel()
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

// CORS
func CORS(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Recovery
func Recovery(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)
                writeError(w, http.StatusInternalServerError, "internal server error")
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// Helper functions
func writeError(w http.ResponseWriter, status int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "error": map[string]interface{}{
            "code":    status,
            "message": msg,
        },
    })
}

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

// ---------- Main program ----------

func helloHandler(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value(UserContextKey)
    writeJSON(w, http.StatusOK, map[string]interface{}{
        "message": "Hello, " + user.(string),
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/hello", helloHandler)

    // Middleware chain
    handler := Chain(mux,
        Recovery,
        RequestLogging,
        CORS,
        Auth("secret-token"),
        RequestTimeout(5*time.Second),
    )

    log.Println("API started on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}
109 linhas de lógica (limite de 40, somente leitura)

7. Exemplo completo: API do Sistema de Gestão de Bibliotecas

GO
// book_api.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
    "time"
)

// ---------- Models ----------

type Book struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Author    string    `json:"author"`
    ISBN      string    `json:"isbn"`
    Year      int       `json:"year"`
    Available bool      `json:"available"`
    CreatedAt time.Time `json:"created_at"`
}

type CreateBookRequest struct {
    Title  string `json:"title"`
    Author string `json:"author"`
    ISBN   string `json:"isbn"`
    Year   int    `json:"year"`
}

type UpdateBookRequest struct {
    Title     string `json:"title"`
    Author    string `json:"author"`
    Available *bool  `json:"available"`
}

// ---------- Validator ----------

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

func validateCreateBook(req CreateBookRequest) []ValidationError {
    var errs []ValidationError
    if strings.TrimSpace(req.Title) == "" {
        errs = append(errs, ValidationError{"title", "title is required"})
    }
    if strings.TrimSpace(req.Author) == "" {
        errs = append(errs, ValidationError{"author", "author is required"})
    }
    if strings.TrimSpace(req.ISBN) == "" {
        errs = append(errs, ValidationError{"isbn", "ISBN is required"})
    }
    if req.Year < 1000 || req.Year > 2100 {
        errs = append(errs, ValidationError{"year", "year must be between 1000 and 2100"})
    }
    return errs
}

// ---------- Store ----------

type BookStore struct {
    mu     sync.RWMutex
    books  map[int]Book
    nextID int
}

func NewBookStore() *BookStore {
    return &BookStore{
        books:  make(map[int]Book),
        nextID: 1,
    }
}

func (s *BookStore) List() []Book {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]Book, 0, len(s.books))
    for _, b := range s.books {
        result = append(result, b)
    }
    return result
}

func (s *BookStore) Create(req CreateBookRequest) (Book, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    // Check ISBN uniqueness
    for _, b := range s.books {
        if b.ISBN == req.ISBN {
            return Book{}, fmt.Errorf("ISBN already exists: %s", req.ISBN)
        }
    }

    book := Book{
        ID:        s.nextID,
        Title:     req.Title,
        Author:    req.Author,
        ISBN:      req.ISBN,
        Year:      req.Year,
        Available: true,
        CreatedAt: time.Now(),
    }
    s.nextID++
    s.books[book.ID] = book
    return book, nil
}

func (s *BookStore) Get(id int) (Book, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    b, ok := s.books[id]
    return b, ok
}

func (s *BookStore) Update(id int, req UpdateBookRequest) (Book, bool, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    b, ok := s.books[id]
    if !ok {
        return Book{}, false, nil
    }
    if req.Title != "" {
        b.Title = req.Title
    }
    if req.Author != "" {
        b.Author = req.Author
    }
    if req.Available != nil {
        b.Available = *req.Available
    }
    s.books[id] = b
    return b, true, nil
}

func (s *BookStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.books[id]
    if !ok {
        return false
    }
    Excluir(s.books, id)
    return true
}

// ---------- API Response ----------

type APIResponse struct {
    Data  interface{} `json:"data,omitempty"`
    Error *APIError   `json:"error,omitempty"`
    Meta  *Meta       `json:"meta,omitempty"`
}

type APIError struct {
    Code    int               `json:"code"`
    Message string            `json:"message"`
    Details []ValidationError `json:"details,omitempty"`
}

type Meta struct {
    Total int `json:"total"`
}

func respond(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Data: data})
}

func respondError(w http.ResponseWriter, status int, msg string, details ...[]ValidationError) {
    err := APIError{Code: status, Message: msg}
    if len(details) > 0 {
        err.Details = details[0]
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Error: &err})
}

// ---------- Handlers ----------

type BookHandler struct {
    store *BookStore
}

func NewBookHandler(store *BookStore) *BookHandler {
    return &BookHandler{store: store}
}

func (h *BookHandler) Register(mux *http.ServeMux, basePath string) {
    mux.HandleFunc("GET "+basePath, h.ListBooks)
    mux.HandleFunc("POST "+basePath, h.CreateBook)
    mux.HandleFunc("GET "+basePath+"/{id}", h.GetBook)
    mux.HandleFunc("PUT "+basePath+"/{id}", h.UpdateBook)
    mux.HandleFunc("DELETE "+basePath+"/{id}", h.DeleteBook)
}

func (h *BookHandler) ListBooks(w http.ResponseWriter, r *http.Request) {
    books := h.store.List()
    respond(w, http.StatusOK, map[string]interface{}{
        "item": books,
        "meta":  Meta{Total: len(books)},
    })
}

func (h *BookHandler) CreateBook(w http.ResponseWriter, r *http.Request) {
    var req CreateBookRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    if errs := validateCreateBook(req); len(errs) > 0 {
        respondError(w, http.StatusUnprocessableEntity, "validation failed", errs)
        return
    }

    book, err := h.store.Create(req)
    if err != nil {
        respondError(w, http.StatusConflict, err.Error())
        return
    }

    respond(w, http.StatusCreated, book)
}

func (h *BookHandler) GetBook(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        respondError(w, http.StatusBadRequest, "invalid book ID")
        return
    }

    book, ok := h.store.Get(id)
    if !ok {
        respondError(w, http.StatusNotFound, "book not found")
        return
    }

    respond(w, http.StatusOK, book)
}

func (h *BookHandler) UpdateBook(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        respondError(w, http.StatusBadRequest, "invalid book ID")
        return
    }

    var req UpdateBookRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        respondError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    book, ok, err := h.store.Update(id, req)
    if err != nil {
        respondError(w, http.StatusInternalServerError, err.Error())
        return
    }
    if !ok {
        respondError(w, http.StatusNotFound, "book not found")
        return
    }

    respond(w, http.StatusOK, book)
}

func (h *BookHandler) DeleteBook(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        respondError(w, http.StatusBadRequest, "invalid book ID")
        return
    }

    if !h.store.Delete(id) {
        respondError(w, http.StatusNotFound, "book not found")
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

// ---------- Middleware ----------

func RequestLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
        next.ServeHTTP(w, r)
        log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Recovery(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)
                respondError(w, http.StatusInternalServerError, "internal server error")
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// ---------- Main ----------

func main() {
    store := NewBookStore()
    handler := NewBookHandler(store)

    mux := http.NewServeMux()
    handler.Register(mux, "/api/v1/books")

    // Health check
    mux.HandleFunc("GET /api/health", func(w http.ResponseWriter, r *http.Request) {
        respond(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    // Middleware chain
    app := Recovery(RequestLogging(mux))

    log.Println("Book API started on :8080")
    log.Println("Endpoints:")
    log.Println("  GET    /api/v1/books       — Book list")
    log.Println("  POST   /api/v1/books       — Create book")
    log.Println("  GET    /api/v1/books/{id}  — View book")
    log.Println("  PUT    /api/v1/books/{id}  — Update book")
    log.Println("  DELETE /api/v1/books/{id}  — Delete book")
    log.Fatal(http.ListenAndServe(":8080", app))
}
100%
graph LR
    subgraph API [REST API Layer]
        H[Handler]
        M[Middleware Chain]
        V[Validator]
    end
    subgraph Store [Store Layer]
        R[Repository<br/>map + RWMutex]
    end
    subgraph Model [Model Layer]
        Book
        User
        Order
    end
    Request --> M
    M --> H
    H --> V
    V --> R
    R --> Book
🔥 Erro comum: Tenha cuidado ao usar interface{} como tipo de resposta — o valor zero de um mapa é serializado como null em JSON, e não como {}. Uma boa prática é definir uma estrutura de resposta explícita (como APIResponse{Data, Error, Meta}) para garantir que os campos apareçam quando tiverem valores e sejam omitidos quando estiverem vazios (omitempty).


❓ Perguntas Frequentes

P: Como se faz o controle de versão de uma API RESTful? R: Existem quatro maneiras: (1) Caminho da URL /api/v1/ (a mais comum); (2) Cabeçalho da solicitação Accept: application/vnd.api+json; version=1; (3) Parâmetro de consulta ?v=1; (4) Subdomínio v1.api.example.com. Recomenda-se o caminho da URL — é o mais intuitivo e envolve os menores custos de desenvolvimento e depuração.

P: Como podemos padronizar a formatação dos erros? R: Defina uma estrutura APIError que inclua os campos código, mensagem e detalhes. Todos os manipuladores devem usar a função padronizada respondError(w, status, msg). O front-end precisa apenas verificar if resp.error para exibir o erro, sem se preocupar com a API específica.

P: A validação de parâmetros deve ser realizada na camada do manipulador ou na camada do serviço? R: A validação básica (campos obrigatórios, formato) é realizada na camada do manipulador, enquanto a validação de negócios (exclusividade, permissões) é realizada na camada do serviço. Se a validação falhar na camada do manipulador, é retornado um código de status 400 ou 422; se falhar na camada de serviço, é retornado um código de status 409 ou 403.

P: Qual é a diferença entre PUT e PATCH? R: PUT é uma substituição completa — o cliente envia o recurso na íntegra, e quaisquer campos ausentes são tratados como reinicializados. PATCH é uma atualização parcial — o cliente envia apenas os campos a serem modificados. Em termos de implementação, o PUT é mais simples, enquanto o PATCH exige o tratamento da fusão de campos parciais. Recomenda-se usar o PUT para operações CRUD e o PATCH para atualizações complexas.

P: Como faço para lidar com um erro 404 “nenhuma rota encontrada”? R: Por padrão, o ServeMux retorna uma página 404. Personalização: crie um manipulador genérico: mux.HandleFunc("/", func(w, r) { writeError(w, 404, "not found") }). Observe que esse manipulador deve ser registrado por último, pois o ServeMux utiliza o roteamento de melhor correspondência.

P: Como a paginação é implementada? R: Usando os parâmetros de consulta ?page=1&per_page=20. O Handler analisa os parâmetros, e a camada Store executa a consulta com LIMIT e OFFSET. A resposta retorna meta: {total, page, per_page} para que o front-end calcule o componente de paginação. A sintaxe {path...} no roteamento do Go 1.22 não é adequada para parâmetros de paginação — os parâmetros devem estar na string de consulta.

P: É necessário implementar o HATEOAS (Hypermedia-As-a-Service)? R: Não. Na prática, o HATEOAS raramente é utilizado em APIs REST. A maioria das APIs precisa apenas retornar dados e metadados dos recursos. O front-end sabe o que fazer a seguir com base na documentação da API; ele não precisa que a API lhe diga “o que pode fazer”.


📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade ⭐): Crie uma API REST para autores. Requisitos: (1) Funcionalidade CRUD completa; (2) Use o caminho /api/v1/authors; (3) Formato padronizado de erros; (4) Validação básica de parâmetros (o nome é obrigatório). Teste todos os endpoints usando o curl.

  2. Exercício avançado (Dificuldade ⭐⭐): Implemente uma API de artigos que associe artigos a autores. Requisitos: (1) POST /articles para criar um artigo (associado a um autor existente); (2) GET /articles?author_id=X para filtrar por autor; (3) Suportar paginação (usando os parâmetros page e per_page); (4) Usar um formato de resposta uniforme: {data, meta}; (5) Validar se title e content não estão vazios.

  3. Desafio (Dificuldade: ⭐⭐⭐): Implemente um sistema de gerenciamento de usuários com uma cadeia completa de middleware. Requisitos: (1) Rotas: usuários (CRUD) + autenticação (login/cadastro); (2) Middleware: Recuperação → Logging → CORS → RateLimit (token bucket) → Autenticação (Bearer Token) → Timeout; (3) Criptografe senhas usando bcrypt durante o cadastro; retorne um JWT ao fazer login; (4) O middleware de autenticação deve analisar o userID do JWT e injetá-lo no Context; (5) Use -race para verificar a segurança de concorrência.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%