Construindo uma API JSON
Aula sobre construção de uma API JSON em Go, cobrindo decodificação de requisições, codificação de respostas, status codes e estruturação do projeto. Inclui exemplos práticos, referências oficiais e exercícios com respostas.
Nesta aula, você aprenderá a construir uma API JSON completa usando a linguagem Go. Vamos explorar desde a decodificação de requisições, passando pela codificação de respostas, até a definição de status codes apropriados e a estruturação de um projeto real. Ao final, você terá uma base sólida para criar serviços web robustos e eficientes.
Go é uma linguagem excelente para construir APIs devido à sua simplicidade, desempenho e suporte nativo para concorrência. Com a biblioteca padrão net/http e os pacotes encoding/json, podemos criar endpoints que lidam com JSON de forma elegante. Vamos mergulhar em cada aspecto, com exemplos práticos que você pode testar imediatamente.
Decodificando requisições
Quando um cliente envia dados para sua API, é comum que eles estejam no corpo da requisição, formatados como JSON. Para extrair esses dados e transformá-los em estruturas Go, utilizamos o pacote encoding/json. A função json.NewDecoder nos permite ler o corpo da requisição e decodificá-lo diretamente em uma struct.
Um ponto crucial é validar a entrada. O Go nos ajuda com tags de struct para mapear campos JSON e também podemos verificar se a decodificação foi bem-sucedida. Se o JSON for inválido ou não corresponder à estrutura esperada, devemos responder com um status code apropriado, como 400 Bad Request.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Processar o usuário...
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}Observe que usamos json.NewDecoder em vez de json.Unmarshal porque ele é mais eficiente para ler de um stream, como o corpo da requisição. Também é importante fechar o corpo da requisição para evitar vazamento de recursos, embora o Go faça isso automaticamente quando o handler retorna.
Se a requisição tiver um corpo vazio ou um JSON malformado, a decodificação retornará um erro. Nesse caso, devemos responder com uma mensagem de erro clara e o status code correto.
Codificando respostas
Assim como decodificamos, precisamos codificar as respostas da API em JSON. O pacote encoding/json também oferece json.NewEncoder, que escreve diretamente no http.ResponseWriter. Isso simplifica o processo e garante que o JSON seja produzido corretamente.
É importante configurar o cabeçalho Content-Type para application/json antes de enviar a resposta. Isso informa ao cliente que o corpo é JSON. Além disso, podemos definir o status code antes de enviar a resposta usando w.WriteHeader.
func getUserHandler(w http.ResponseWriter, r *http.Request) {
user := User{Name: "Alice", Email: "alice@example.com"}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(user)
}Uma boa prática é criar funções auxiliares para escrever respostas JSON, evitando repetição de código. Por exemplo, uma função writeJSON que recebe o status code e os dados. Isso torna o código mais limpo e consistente.
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}Dessa forma, todos os handlers podem usar essa função, garantindo que as respostas sejam uniformes.
Status codes
Os status codes HTTP são fundamentais para comunicar o resultado de uma requisição. Em uma API JSON, é crucial usar os códigos corretos para que o cliente saiba o que aconteceu. Os mais comuns são:
200 OK– requisição bem-sucedida.201 Created– recurso criado com sucesso.400 Bad Request– requisição inválida (ex.: JSON malformado).404 Not Found– recurso não encontrado.500 Internal Server Error– erro inesperado no servidor.
No Go, usamos as constantes do pacote net/http, como http.StatusOK, http.StatusCreated, etc. Isso evita erros de digitação e torna o código mais legível.
func getUserByIDHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[len("/users/"):]
user, err := findUserByID(id)
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "User not found"})
return
}
writeJSON(w, http.StatusOK, user)
}É importante não expor detalhes internos em mensagens de erro. Em vez de retornar o erro real, envie uma mensagem genérica e registre o erro no servidor (ex.: usando log). Isso melhora a segurança.
Estrutura
Para projetos reais, é fundamental organizar o código de forma modular. Uma estrutura comum para uma API em Go é separar em pastas por responsabilidade: handlers, models, repository, etc. Isso facilita a manutenção e o teste.
Vamos criar um exemplo simples com a seguinte estrutura:
meu-projeto/
├── main.go
├── handlers/
│ └── user.go
├── models/
│ └── user.go
└── repository/
└── user.go
No main.go, configuramos as rotas e inicializamos o servidor. Nos handlers, definimos as funções que recebem http.ResponseWriter e *http.Request. Nos models, definimos as structs. No repository, implementamos a lógica de acesso a dados (pode ser um banco ou memória).
// main.go
package main
import (
"log"
"net/http"
"meu-projeto/handlers"
)
func main() {
http.HandleFunc("/users", handlers.GetUsers)
http.HandleFunc("/users/", handlers.GetUserByID)
http.HandleFunc("/users/create", handlers.CreateUser)
log.Println("Server started on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Essa separação permite que cada parte seja testada isoladamente. Além disso, podemos usar interfaces para abstrair o repositório, facilitando a troca de implementação (ex.: de memória para banco de dados).
Boas práticas
Ao construir APIs JSON em Go, algumas boas práticas incluem:
- Usar tags de struct para controlar a serialização JSON.
- Definir códigos de status precisos para cada situação.
- Validar entradas e retornar erros claros.
- Estruturar o projeto em camadas.
- Escrever testes para handlers e repositórios.
- Manter as respostas consistentes (ex.: sempre ter um campo
errorquando houver erro).
Referências
- Documentação oficial do pacote encoding/json
- Documentação oficial do pacote net/http
- Effective Go
- How to properly parse a JSON request body
- How to properly encode a JSON response
- MDN HTTP Status Codes
Exercícios
Escreva um handler que decodifica uma requisição JSON contendo um campo
titleecontentde um post, e retorna o post com um ID gerado. Use status code 201.✓ Resposta:type Post struct { ID int `json:"id"` Title string `json:"title"` Content string `json:"content"` } func createPostHandler(w http.ResponseWriter, r *http.Request) { var post Post if err := json.NewDecoder(r.Body).Decode(&post); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"}) return } post.ID = time.Now().UnixNano() // exemplo writeJSON(w, http.StatusCreated, post) }Crie uma função que retorna uma lista de usuários em JSON. A lista deve ser um slice de structs.
✓ Resposta:func getUsersHandler(w http.ResponseWriter, r *http.Request) { users := []User{ {Name: "Alice", Email: "alice@example.com"}, {Name: "Bob", Email: "bob@example.com"}, } writeJSON(w, http.StatusOK, users) }Implemente um handler que retorna 404 se um recurso não for encontrado, e 200 se encontrado. Use um map para armazenar os recursos.
✓ Resposta:var users = map[string]User{ "1": {Name: "Alice", Email: "alice@example.com"}, "2": {Name: "Bob", Email: "bob@example.com"}, } func getUserByIDHandler(w http.ResponseWriter, r *http.Request) { id := r.URL.Path[len("/users/"):] user, ok := users[id] if !ok { writeJSON(w, http.StatusNotFound, map[string]string{"error": "User not found"}) return } writeJSON(w, http.StatusOK, user) }Modifique o handler de criação para validar se os campos obrigatórios não estão vazios. Se estiverem, retorne 400.
✓ Resposta:func createUserHandler(w http.ResponseWriter, r *http.Request) { var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"}) return } if user.Name == "" || user.Email == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Name and email are required"}) return } // salvar e retornar writeJSON(w, http.StatusCreated, user) }Estruture um projeto Go com pastas
handlers,modelserepositorypara uma API de produtos. Escreva um handler de criação.✓ Resposta:// models/product.go package models type Product struct { ID int `json:"id"` Name string `json:"name"` Price float64 `json:"price"` } // repository/product.go package repository import "meu-projeto/models" var products = []models.Product{} func SaveProduct(p models.Product) models.Product { p.ID = len(products) + 1 products = append(products, p) return p } // handlers/product.go package handlers import ( "encoding/json" "net/http" "meu-projeto/models" "meu-projeto/repository" ) func CreateProductHandler(w http.ResponseWriter, r *http.Request) { var p models.Product if err := json.NewDecoder(r.Body).Decode(&p); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"}) return } saved := repository.SaveProduct(p) writeJSON(w, http.StatusCreated, saved) } // main.go package main import ( "log" "net/http" "meu-projeto/handlers" ) func main() { http.HandleFunc("/products", handlers.CreateProductHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }