Construindo uma API JSON
Nesta aula, você aprenderá a construir uma API JSON em Go do zero, cobrindo desde a decodificação de requisições até a codificação de respostas, o uso correto de status codes e a estruturação de um projeto. Ao final, você terá uma base sólida para criar APIs RESTful eficientes e bem organizadas.
Nesta aula, vamos mergulhar no desenvolvimento de APIs JSON utilizando a linguagem Go. Você aprenderá os conceitos fundamentais para criar endpoints HTTP que recebem e enviam dados no formato JSON, que é o formato mais comum para comunicação entre serviços web. Vamos explorar desde a decodificação de requisições até a codificação de respostas, passando pelos status codes HTTP e a organização do código.
Construir uma API JSON em Go é uma tarefa direta graças à biblioteca padrão, que oferece pacotes como encoding/json e net/http. No entanto, para criar APIs robustas e de fácil manutenção, é importante seguir boas práticas de estruturação e tratamento de erros. Nesta aula, você verá exemplos práticos e completos que poderá adaptar para seus próprios projetos.
Decodificando requisições
Quando um cliente envia dados para a API, geralmente em uma requisição POST ou PUT, o corpo da requisição contém dados no formato JSON. Para acessar esses dados em Go, utilizamos o pacote encoding/json e a função json.NewDecoder ou json.Unmarshal.
O processo básico envolve definir uma struct que represente a estrutura dos dados esperados, criar uma variável dessa struct e decodificar o corpo da requisição. É importante verificar se a decodificação foi bem-sucedida e se os dados são válidos.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
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 request body", http.StatusBadRequest)
return
}
// processa o usuário...
fmt.Fprintf(w, "User created: %+v", user)
}No exemplo acima, definimos uma struct User com tags JSON que mapeiam os campos do JSON para os campos da struct. A função Decode lê o corpo da requisição e tenta preencher a struct. Se houver erro, como JSON malformado ou campos incompatíveis, retornamos um erro 400 (Bad Request).
Além disso, podemos usar json.Unmarshal lendo todo o corpo com io.ReadAll, mas Decode é mais eficiente para streaming. Para validações adicionais, como verificar campos obrigatórios, podemos implementar funções de validação após a decodificação.
Codificando respostas
Para enviar dados de volta ao cliente em formato JSON, usamos o pacote encoding/json novamente, mas agora com json.NewEncoder ou json.Marshal. A função Encode escreve diretamente no ResponseWriter, enquanto Marshal gera os bytes que podemos manipular antes de enviar.
É fundamental definir a struct de resposta com as tags JSON corretas e, se necessário, omitir campos vazios com a tag omitempty. Também devemos definir o cabeçalho Content-Type como application/json para que o cliente saiba que a resposta é JSON.
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
// busca o usuário no banco...
user := UserResponse{ID: 1, Name: "João", Email: "joao@example.com", Age: 30}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(user)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
}No exemplo, usamos NewEncoder(w).Encode(user) que serializa a struct e escreve no ResponseWriter. Definimos o cabeçalho antes de enviar. Se houver erro na serialização (raro, mas possível), retornamos 500.
Para listas de objetos, podemos passar um slice de structs. O encoder cuidará de gerar o JSON array.
users := []UserResponse{
{ID: 1, Name: "João", Email: "joao@example.com", Age: 30},
{ID: 2, Name: "Maria", Email: "maria@example.com", Age: 25},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)Status codes
Os status codes HTTP são essenciais para comunicar o resultado de uma requisição. Em Go, usamos a função http.Error para enviar mensagens de erro com um código, ou definimos o código manualmente com w.WriteHeader.
Os códigos mais comuns em APIs JSON incluem: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error. Cada um tem um significado específico e deve ser usado corretamente.
func getUserByIDHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[len("/users/"):]
user, err := findUserByID(id)
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(user)
}No exemplo, se o usuário não for encontrado, retornamos 404 com uma mensagem. Se encontrado, definimos o status 200 e codificamos a resposta.
Para criação de recursos, o status 201 Created é apropriado, e podemos incluir o cabeçalho Location com a URL do novo recurso.
func createUserHandler(w http.ResponseWriter, r *http.Request) {
// decodifica e valida...
newUser, err := createUser(user)
if err != nil {
http.Error(w, "Failed to create user", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Location", fmt.Sprintf("/users/%d", newUser.ID))
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newUser)
}Estrutura
Para manter o código organizado e escalável, é comum estruturar a API em pacotes e handlers separados. Uma estrutura típica para uma API em Go inclui:
- main.go: configuração do servidor, rotas e inicialização.
- handlers: funções que lidam com as requisições HTTP.
- models: definição de structs que representam os dados.
- storage: camada de acesso a dados (banco, memória, etc.).
Um exemplo de organização de pastas:
meuprojeto/
├── main.go
├── handlers/
│ └── user.go
├── models/
│ └── user.go
└── storage/
└── memory.goNo main.go, configuramos as rotas usando o pacote net/http ou frameworks como gorilla/mux ou chi. Para esta aula, usaremos a biblioteca padrão.
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/users", userHandler)
http.HandleFunc("/users/", userByIDHandler)
log.Println("Server running on port 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Em handlers/user.go, definimos as funções handler. Podemos ter um handler para listar, criar, atualizar e deletar usuários, cada um com sua lógica.
package handlers
import (
"encoding/json"
"net/http"
)
func userHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
listUsers(w, r)
case http.MethodPost:
createUser(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func userByIDHandler(w http.ResponseWriter, r *http.Request) {
// ...
}Essa separação facilita a manutenção e teste. Além disso, podemos criar um middleware para logging, autenticação, etc.
Boas práticas
Ao construir APIs JSON em Go, considere as seguintes boas práticas:
- Sempre defina o cabeçalho
Content-Type: application/jsonnas respostas. - Use status codes apropriados para cada situação.
- Valide os dados recebidos antes de processá-los.
- Estruture seu projeto em pacotes lógicos.
- Utilize tags JSON nas structs para controlar a serialização.
- Trate erros de forma consistente, retornando mensagens claras.
- Considere usar frameworks como
chiouginpara rotas mais avançadas, mas a biblioteca padrão é suficiente para muitos casos.
Exercícios
Crie uma função handler que decodifique uma requisição JSON com os campos
titleecontentde um post e imprima os valores no console.✓ Resposta:type Post struct { Title string `json:"title"` Content string `json:"content"` } func postHandler(w http.ResponseWriter, r *http.Request) { var post Post err := json.NewDecoder(r.Body).Decode(&post) if err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } fmt.Printf("Title: %s, Content: %s\n", post.Title, post.Content) }Escreva um handler que retorne uma lista de produtos em JSON, com os campos
id,nameeprice.✓ Resposta:type Product struct { ID int `json:"id"` Name string `json:"name"` Price float64 `json:"price"` } func productsHandler(w http.ResponseWriter, r *http.Request) { products := []Product{ {ID: 1, Name: "Laptop", Price: 3999.99}, {ID: 2, Name: "Mouse", Price: 99.90}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(products) }Implemente um handler que retorne o status 201 Created ao criar um novo usuário, e inclua o cabeçalho
Locationcom a URL do recurso.✓ Resposta:func createUserHandler(w http.ResponseWriter, r *http.Request) { // decodifica user... user.ID = 1 // suponha que o ID foi gerado w.Header().Set("Content-Type", "application/json") w.Header().Set("Location", fmt.Sprintf("/users/%d", user.ID)) w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) }Explique como você validaria se o campo
emailde uma requisição está no formato correto antes de processar.✓ Resposta: Usaria a funçãomail.ParseAddressdo pacotenet/mailpara validar o formato. Por exemplo:import "net/mail" if _, err := mail.ParseAddress(user.Email); err != nil { http.Error(w, "Invalid email", http.StatusBadRequest) return }Escreva um middleware simples que logue o método e a URL de cada requisição antes de chamar o próximo handler.
✓ Resposta:func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) }) }