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.go

No 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/json nas 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 chi ou gin para rotas mais avançadas, mas a biblioteca padrão é suficiente para muitos casos.

Exercícios

  1. Crie uma função handler que decodifique uma requisição JSON com os campos title e content de 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)
    }
    
  2. Escreva um handler que retorne uma lista de produtos em JSON, com os campos id, name e price.

    ✓ 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)
    }
    
  3. Implemente um handler que retorne o status 201 Created ao criar um novo usuário, e inclua o cabeçalho Location com 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)
    }
    
  4. Explique como você validaria se o campo email de uma requisição está no formato correto antes de processar.

    ✓ Resposta: Usaria a função mail.ParseAddress do pacote net/mail para validar o formato. Por exemplo:
    import "net/mail"
    
    if _, err := mail.ParseAddress(user.Email); err != nil {
        http.Error(w, "Invalid email", http.StatusBadRequest)
        return
    }
    
  5. 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)
        })
    }
    

Referências