CodeGym /Cursos /Go SELF /Design de recursos: /api/v1/tasks, /api/v1/tasks/{id} e /...

Design de recursos: /api/v1/tasks, /api/v1/tasks/{id} e /done

Go SELF
Nível 56, Lição 2
Disponível

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
GET /getTasks
GET /api/v1/tasks
criar uma tarefa
POST /createTask
POST /api/v1/tasks
obter a tarefa 7
GET /getTask?id=7
GET /api/v1/tasks/7
marcar a tarefa 7 como concluida
POST /markDone?id=7
POST /api/v1/tasks/7/done

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:

  1. Fazer a alteracao atualizando a tarefa (por exemplo, parcialmente) e passar {"done": true} no corpo.
  2. 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".

1
Tarefa
Go SELF, nível 56, lição 2
Bloqueado
Caminho da coleção
Caminho da coleção
1
Tarefa
Go SELF, nível 56, lição 2
Bloqueado
Caminhos da tarefa
Caminhos da tarefa
1
Tarefa
Go SELF, nível 56, lição 2
Bloqueado
Mapa de rotas
Mapa de rotas
1
Tarefa
Go SELF, nível 56, lição 2
Bloqueado
Validador de URL
Validador de URL
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION