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á
- Princípios de projeto RESTful (Recursos/Verbos/Códigos de status)
- Criação de uma API REST com rotas do Go 1.22
- Validação dos parâmetros da solicitação
- Integração da cadeia de middleware
- Padronizar o formato da resposta de erro
- Política de gerenciamento de versões da API
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!’”
// 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
// 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
// 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
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)
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))
}
5. Validação de parâmetros
(1) ▶ Exemplo: Validação estruturada
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())
}
}
6. Integração da cadeia de middleware
(1) ▶ Exemplo: cadeia de middleware de API
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))
}
7. Exemplo completo: API do Sistema de Gestão de Bibliotecas
// 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))
}
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
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çãoAccept: application/vnd.api+json; version=1; (3) Parâmetro de consulta?v=1; (4) Subdomíniov1.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
APIErrorque inclua os campos código, mensagem e detalhes. Todos os manipuladores devem usar a função padronizadarespondError(w, status, msg). O front-end precisa apenas verificarif resp.errorpara 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 retornameta: {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
- Design REST: Recurso (substantivo) + Método (verbo) + Código de status
- Gerenciamento de versões de URL: a abordagem
/api/v1/é altamente recomendada - Padronizar o formato do erro:
{error: {code, message, details}} - Validação de parâmetros: Validação básica (Handler) + Validação de negócios (Serviço)
- Cadeia de middleware: Recuperação → Registro em log → CORS → Autenticação → Tempo limite → Manipulador
- Funções de resposta unificadas:
respond()/respondError() - Paginação:
?page=N&per_page=N+ metainformações
📝 Exercícios
-
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. -
Exercício avançado (Dificuldade ⭐⭐): Implemente uma API de artigos que associe artigos a autores. Requisitos: (1)
POST /articlespara criar um artigo (associado a um autor existente); (2)GET /articles?author_id=Xpara filtrar por autor; (3) Suportar paginação (usando os parâmetrospageeper_page); (4) Usar um formato de resposta uniforme:{data, meta}; (5) Validar setitleecontentnão estão vazios. -
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
-racepara verificar a segurança de concorrência.