1. Por que pensar em recursos, e nao em comandos
Quando voce comeca a construir uma HTTP-API, e muito facil cair no estilo de "comandos convenientes": /createTask, /getTask, /markTaskDone. Isso parece um painel de controle: botao "Criar", botao "Obter", botao "Deixar bonito". O problema e que o HTTP ja inventou os botoes para voce — estes sao os metodos. E o caminho deve descrever que entidade esta nesse endereco.
A abordagem orientada a recursos e quando voce imagina a API como uma "vitrine de entidades". Ha uma colecao de tarefas, ha uma tarefa especifica, ha uma representacao do estado "concluido". Assim, fica mais facil para o cliente prever a API "pela forma", em vez de decorar 30 comandos URL exclusivos.
A boa noticia: voce nao precisa conhecer "toda a filosofia REST". Basta guardar uma frase simples na memoria:
O URL responde a pergunta "o que e isso?", e o metodo responde "o que fazer?"
Mini-comparacao: estilo de comando vs. orientado a recursos
| O que queremos | URL de comando (ruim) | URL orientado a recursos (ok) |
|---|---|---|
| obter a lista de tarefas | |
|
| criar uma tarefa | |
|
| obter a tarefa 7 | |
|
| marcar a tarefa 7 como concluida | |
|
O estilo de comando parece "claro" ate o momento em que a API cresce. Depois comeca a deriva: createTask, addTask, newTask, makeTask — e, de repente, sua API vira um curso linguistico sobre "sinonimos de verbos em ingles".
2. A versao no caminho: por que /api/v1 e necessario
Versionamento no caminho muitas vezes parece burocracia: "acabamos de comecar, que versoes?". Mas o problema e que e quase impossivel nao mudar uma API. Voce vai querer adicionar um campo, renomear um campo, mudar o formato, ou "ajustar um pouquinho" o comportamento dos erros. E e ai que a coisa fica interessante: clientes antigos podem continuar vivos por meses.
Existe uma regra de ouro: se o cliente ja se integrou, sua "pequena mudanca" pode virar uma queda em producao numa sexta-feira a noite. E sexta-feira a noite e um horario sagrado, quando o ideal e nem encostar em producao.
No mundo Go, a importancia da compatibilidade fica bem clara: se voce muda uma API publica (funcoes, tipos), voce corre o risco de quebrar o codigo de outras pessoas que dependem dela. Isso costuma ser descrito diretamente como um problema de compatibilidade: "nao se pode retirar a API, senao os programas quebram". Com contratos HTTP a historia e a mesma, so que o "programa de outra pessoa" e o cliente da sua API.
Por isso /api/v1 e uma forma honesta de dizer: "estas sao as regras da versao 1". Se um dia forem necessarias mudancas incompativeis, voce tera a opcao /api/v2, e os clientes antigos nao precisam sofrer.
Pequeno exemplo em Go: fixando o prefixo de versao
package main
const apiV1 = "/api/v1"
func tasksPath() string {
return apiV1 + "/tasks"
}
Aqui nao ha magia — apenas um habito: construimos todos os caminhos a partir de um prefixo unico. Isso reduz o risco de erros de digitacao e ajuda a manter o contrato consistente.
3. Colecao de tarefas: /api/v1/tasks
O caminho /api/v1/tasks e uma colecao de tarefas. Imagine uma prateleira numa loja: nela estao todos os produtos (tarefas). Normalmente fazemos duas coisas com a prateleira: "ver o que ha nela" e "colocar um novo item".
Do mesmo modo na API: listar tarefas e uma consulta a colecao, criar uma tarefa e adicionar algo a colecao. O metodo distingue essas acoes, e o caminho permanece o mesmo.
Aqui ha um aspecto psicologico importante: quando o caminho e unico e o metodo muda, a API parece um "formulario", e nao um "zoologico de links". Para o cliente, e mais facil memorizar /tasks e depois deduzir logicamente: GET — leio, POST — crio.
Exemplo: descrevendo um endpoint como objeto de contrato
package main
type Endpoint struct {
Method string
Path string
}
var listTasks = Endpoint{Method: "GET", Path: "/api/v1/tasks"}
var createTask = Endpoint{Method: "POST", Path: "/api/v1/tasks"}
Se voce pensou: "por que eu precisaria disso em codigo Go, se ainda nem escrevemos o servidor?", a resposta e simples: assim voce treina o musculo de "contrato primeiro". Essa mesma lista pode depois ser usada na documentacao, nos testes e ate nos logs.
Por que tasks esta no plural
/tasks, e nao /task, porque uma colecao implica multiplicidade. Sim, e uma "regra de boa educacao", mas ela realmente ajuda o cerebro. Se voce ve o plural, espera uma lista. Se ve /tasks/7, espera um unico item.
4. Uma tarefa: /api/v1/tasks/{id}
Se /tasks e a prateleira, /tasks/{id} e o cartao de uma tarefa especifica. Aqui {id} e um template na documentacao, e nao as chaves literais no URL real.
Ou seja, no contrato voce escreve:
- /api/v1/tasks/{id}
e na requisicao real o cliente substitui por um numero:
- /api/v1/tasks/7
O que {id} significa
id e um identificador. No nosso dominio didatico, o mais simples e combinar que id e um numero inteiro (por exemplo, int). O importante e justamente "combinar e fixar": o cliente precisa entender o que deve colocar no caminho.
Se o formato nao for fixado, vira caos: um cliente manda UUID, outro manda numeros, um terceiro manda 0007, e um quarto manda "sete" (porque e otimista).
Exemplo: funcao que materializa um caminho a partir de {id}
package main
import "fmt"
func taskPath(id int) string {
return fmt.Sprintf("/api/v1/tasks/%d", id)
}
Funcoes assim parecem pequenas, mas disciplinam. Quando voce escreve taskPath(7), voce nao esquece /api/v1, nao confunde tasks com task, e nao adiciona uma barra extra. E essas funcoes tambem sao muito boas em testes: voce compara uma string, em vez de monta-la manualmente em cada lugar.
5. O estado done: campo e caminho /done
Com done costuma surgir a primeira discussao mais adulta sobre design de API. Por que? Porque done parece apenas um campo booleano na tarefa (done: true/false), mas o usuario quer um botao separado de "Marcar como concluida", e o desenvolvedor quer um endpoint separado, simples e claro.
No nosso dominio, queremos suportar um cenario claro: "marcar uma tarefa como concluida". E, no contexto do curso, escolhemos o exemplo de caminho para estado:
- /api/v1/tasks/{id}/done
Isso se le quase como uma frase: "done da tarefa {id}". Aqui /done funciona como um subrecurso ou "representacao de estado". Nao estamos fingindo que done e uma entidade separada no nivel de /done/{id}. Estamos mantendo isso dentro da tarefa.
Por que /done e normal
Antes criticamos os "verbos no URL" (/createTask). Mas /done e um caso diferente: nao e um "comando para o servidor", e parte do modelo do recurso — o estado. E como se dissessimos: "a tarefa tem um atributo done, e ele tem um lugar separado no caminho".
Voce pode pensar assim: /api/v1/tasks/7/done e a "alavanca" do estado de conclusao da tarefa 7.
Variantes que geralmente sao comparadas
Ha duas abordagens populares:
- Fazer a alteracao atualizando a tarefa (por exemplo, parcialmente) e passar {"done": true} no corpo.
- Criar um endpoint de estado separado: /tasks/{id}/done.
Dentro desta licao, estamos fixando precisamente o design do caminho /done. Ja a semantica exata do metodo (POST/PATCH) e os detalhes de status e melhor tratarmos como parte do contrato geral, sem entrar fundo na implementacao.
Exemplo: funcao de caminho done para uma tarefa especifica
package main
import "fmt"
func taskDonePath(id int) string {
return fmt.Sprintf("/api/v1/tasks/%d/done", id)
}
Agora voce tem tres "blocos" estaveis:
- tasksPath() — colecao
- taskPath(id) — um recurso
- taskDonePath(id) — estado done
Esse e o "esqueleto" do espaco de URLs da nossa API.
Pequeno diagrama: como tudo se relaciona
Quando tudo isso se mistura na cabeca, uma visualizacao simples ajuda. Nao ha magia de REST aqui, apenas uma arvore.
flowchart TD
A["/api/v1/tasks
Colecao de tarefas"] --> B["/api/v1/tasks/{id}
Uma tarefa"]
B --> C["/api/v1/tasks/{id}/done
Estado done"]
A arvore mostra uma coisa importante: /done vive abaixo da tarefa, e nao ao lado dela. Isso reduz o risco de erros de design, em que done de repente passa a existir como uma entidade separada sem sentido.
6. Consistencia do dicionario: tasks, done, {id}
Muitas vezes, nao sao bugs complicados que estragam uma API, e sim uma pequena inconsistncia. Um endpoint usa /tasks, outro /todo, o terceiro /items. Na sua cabeca isso e tudo a mesma coisa, mas o cliente nao e telepata. Ele le a documentacao ao pe da letra.
Por isso, no dominio didatico, mantemos um unico vocabulario:
- entidade: tasks
- identificador: id
- estado: done
- versao: /api/v1
Se voce quiser adicionar "beleza" e chamar as tarefas de todos, pare e pergunte a si mesmo: "o que o cliente ganha com isso?". Normalmente a resposta e: "nada, exceto mais espaco para erro".
Aqui e util uma analogia com nomes de variaveis em Go: se voce escreve taskID em um lugar, e id em outro, e task_id em um terceiro, o codigo fica mais dificil de ler. O design de URLs e a mesma coisa, so que "as variaveis" sao lidas por outros programas.
7. Mapa da API em codigo Go
Mesmo antes de existir um servidor HTTP real, e util manter o contrato como uma estrutura de dados. Isso ajuda a nao discutir apenas com palavras ("ah, fica la em /done"), mas a ver strings concretas.
Isso tambem reduz as chances de erro de digitacao. Um erro de digitar o URL e um dos bugs mais frustrantes: quase tudo esta certo, mas 404 aparece e voce passa uma hora procurando uma letra extra.
Passo 1: definir operacoes e uma lista de rotas
package main
type Route struct {
Method string
Pattern string
Op string
}
func apiRoutes() []Route {
return []Route{
{Method: "GET", Pattern: "/api/v1/tasks", Op: "list_tasks"},
{Method: "POST", Pattern: "/api/v1/tasks", Op: "create_task"},
{Method: "GET", Pattern: "/api/v1/tasks/{id}", Op: "get_task"},
{Method: "POST", Pattern: "/api/v1/tasks/{id}/done", Op: "mark_done"},
}
}
Observe: usamos {id} justamente como um template de documentacao. Isso e conveniente para leitura e discussao.
Passo 2: imprimir o "mapa" como mini-documentacao
package main
import "fmt"
func printRoutes(routes []Route) {
for _, r := range routes {
fmt.Printf("%s %-25s # %s\n", r.Method, r.Pattern, r.Op)
}
}
Passo 3: montar tudo em main
package main
func main() {
printRoutes(apiRoutes())
}
Se voce executar, a saida sera mais ou menos assim:
GET /api/v1/tasks # list_tasks
POST /api/v1/tasks # create_task
GET /api/v1/tasks/{id} # get_task
POST /api/v1/tasks/{id}/done # mark_done
Isso parece quase um rascunho de README. E essa e a beleza: o contrato deve ser simples o suficiente para caber "em uma unica tela".
Por que mudar caminhos quase sempre doi
Caminhos fazem parte do contrato, e e dificil muda-los sem consequencias. Mesmo que voce tenha "apenas renomeado" /tasks para /todo, os clientes vao parar de funcionar.
No mundo Go, a questao da compatibilidade nao e abstrata. Se uma API publica muda, o codigo alheio quebra, e isso e um problema muito pratico: nao e possivel "pegar a API de volta" sem quebrar coisas. O contrato HTTP se comporta do mesmo jeito: voce nao pode remover um endpoint discretamente se alguem depende dele.
E ha ainda outro bom exemplo de como a compatibilidade influencia o design: quando context.Context foi adicionado a biblioteca padrao do Go, nao dava para simplesmente mudar a assinatura de http.Client.Do, porque isso quebraria muito codigo. Entao o suporte a contexto foi incorporado em http.Request de um jeito que nao quebrasse chamadas antigas.
Essa e uma otima licao: o design da interface (e da API) muitas vezes e definido nao pela "arquitetura perfeita", mas pela responsabilidade com os usuarios.
8. Erros tipicos no design de caminhos de tarefas
Erro #1: URLs de comando (/createTask, /markDone) em vez de recursos.
Normalmente isso acontece pela vontade de fazer "como no RPC": um comando separado para cada acao. No fim, surge um zoologico de caminhos, e o cliente nao consegue prever a API pela forma. A cura e uma regra simples: o caminho e um substantivo (recurso), a acao e o metodo HTTP, e excecoes como /done devem fazer parte do modelo de estado, e nao de um comando aleatorio.
Erro #2: ausencia de um vocabulario unico (/tasks, /todo, /items) e deriva de nomes.
No inicio, isso parece um "detalhe". Depois, de repente, percebe-se que diferentes equipes chamaram a mesma coisa de formas diferentes, e o cliente passa a escrever gambiarra do tipo "se /tasks nao funcionar, tente /todo". O caminho normal e escolher um unico dominio (tasks) e mantelo em todo lugar: nos caminhos, nos DTOs, na documentacao e nos exemplos.
Erro #3: {id} nao definido por formato, e cada cliente entende do seu jeito.
Se voce nao disser que id e um numero inteiro (ou UUID), os clientes vao comecar a adivinhar. Especialmente engraçado quando um cliente manda "7", outro "007", e um terceiro "seven". No contrato, e preciso deixar claro: {id} e, por exemplo, um inteiro positivo. Mesmo que a validacao ainda nao tenha sido implementada, o acordo ja precisa existir.
Erro #4: confusao entre estado e recurso para done.
As vezes done e transformado de repente em um recurso de nivel superior separado (/api/v1/done/{id}), e a API fica estranha: "onde esta a tarefa e onde esta o atributo?". No nosso dominio, done e o estado da tarefa, por isso vive abaixo de /tasks/{id}: /tasks/{id}/done. Assim fica mais facil para o cliente entender que o que muda e justamente o estado da tarefa especifica.
Erro #5: ausencia de versao (/api/tasks em vez de /api/v1/tasks) e a esperanca de que "sempre seremos cuidadosos".
Uma esperança nobre, mas a realidade vence: a API muda. Sem versao, ou voce tera de quebrar clientes antigos, ou precisara carregar muita compatibilidade retroativa sem uma separacao clara de regras. A versao no caminho e uma forma simples e honesta de dizer: "este e o contrato pelo qual respondemos".
GO TO FULL VERSION