Pular para o conteúdo

O que é API REST? Guia Completo para Iniciantes

Se você está começando no mundo da programação, provavelmente já ouviu falar sobre APIs REST em algum momento. Talvez você tenha visto esse termo em vagas de emprego, tutoriais no YouTube, ou até mesmo em conversas com outros desenvolvedores. Mas o que exatamente significa API REST? Por que todo mundo fala sobre isso como se fosse algo indispensável?

Olha, eu entendo perfeitamente essa confusão inicial. Quando comecei a programar, APIs pareciam algo extremamente complicado e técnico demais para um iniciante. Mas a verdade é que, depois que você entende o conceito básico, tudo faz muito mais sentido. E é exatamente isso que vamos fazer neste artigo: desmistificar as APIs REST de um jeito simples e direto.

Pensa comigo: toda vez que você abre o Instagram e vê as fotos dos seus amigos, ou quando pede uma pizza pelo aplicativo, ou mesmo quando consulta a previsão do tempo no seu celular, existe uma API REST trabalhando nos bastidores. Elas são a ponte que conecta diferentes sistemas, permitindo que aplicativos conversem entre si e troquem informações de forma organizada e segura.

Neste guia, vamos explorar desde o básico até conceitos mais avançados, sempre com exemplos práticos e uma linguagem acessível. Vou usar alguns exemplos com NestJS ao longo do artigo, mas quero deixar claro desde já: os conceitos de API REST não são exclusivos desse framework. Você pode aplicar tudo que vamos aprender aqui em qualquer linguagem ou tecnologia, seja Node.js com Express, Python com Django ou Flask, Java com Spring Boot, ou qualquer outra ferramenta que você escolher. O importante é entender a lógica por trás do REST.

O que é uma API?

Antes de entrarmos no REST propriamente dito, precisamos entender o que é uma API. API significa Application Programming Interface, ou em português, Interface de Programação de Aplicações. Eu sei, o nome parece complicado, mas o conceito é mais simples do que você imagina.

Vamos usar uma analogia que sempre funciona bem: pensa em um restaurante. Você está sentado na mesa, com fome, olhando o cardápio. Você sabe que existe uma cozinha ali atrás, com chefs preparando os pratos, mas você não pode simplesmente entrar na cozinha e pegar sua comida, certo? Você precisa de alguém para fazer essa intermediação. É aí que entra o garçom.

O garçom é exatamente como uma API funciona. Ele pega seu pedido (sua requisição), leva até a cozinha (o servidor ou banco de dados), espera o prato ficar pronto (o processamento), e traz de volta para você (a resposta). Você não precisa saber como o chef preparou o prato, quais panelas ele usou, ou qual a temperatura do forno. Você só precisa fazer o pedido e receber sua comida.

No mundo da programação, é a mesma coisa. Uma API permite que diferentes softwares conversem entre si sem que um precise saber exatamente como o outro funciona por dentro. Por exemplo, quando você usa o botão “Entrar com Google” em algum site, esse site está usando a API do Google. O site não tem acesso à sua senha, não sabe como o Google armazena seus dados, ele simplesmente faz um pedido através da API e recebe uma resposta confirmando se você está autenticado ou não.

As APIs são fundamentais no desenvolvimento moderno porque permitem que aproveitemos funcionalidades já existentes sem precisar reinventar a roda. Quer adicionar mapas no seu aplicativo? Use a API do Google Maps. Precisa processar pagamentos? Integre com a API do Stripe ou PagSeguro. Quer mostrar posts de uma rede social? Conecte-se à API dela.

Existem vários tipos de APIs. Algumas funcionam internamente dentro de uma empresa, permitindo que diferentes sistemas da organização conversem entre si. Outras são públicas, disponíveis para qualquer desenvolvedor usar (às vezes de graça, às vezes pagando). E algumas são privadas, apenas para parceiros específicos.

Mas entre todos os tipos e padrões de APIs que existem, o REST se tornou o mais popular e amplamente adotado nos últimos anos. E é sobre ele que vamos falar agora.

O que é REST?

Agora que você já entendeu o conceito de API, vamos falar sobre REST. REST é uma sigla para Representational State Transfer, ou Transferência de Estado Representacional em português. Eu sei, o nome é ainda mais confuso que API, mas relaxa que vou explicar de um jeito que faça sentido.

REST não é uma tecnologia, uma linguagem de programação ou um protocolo rígido. Na verdade, REST é um estilo arquitetural, um conjunto de princípios e boas práticas para criar APIs na web. Foi criado lá em 2000 por um cara chamado Roy Fielding na sua tese de doutorado, e desde então se tornou o padrão mais usado para desenvolvimento de APIs.

Mas por que REST ficou tão popular? Simples: porque ele aproveita a estrutura que a web já usa naturalmente. Sabe quando você digita um endereço no navegador e aperta Enter? Você está fazendo uma requisição HTTP para um servidor, que te devolve uma resposta, geralmente uma página HTML. O REST usa exatamente essa mesma lógica, só que ao invés de devolver páginas HTML bonitinhas para humanos lerem, ele devolve dados estruturados (geralmente em JSON) que outros programas conseguem entender e processar.

A ideia central do REST é tratar tudo como “recursos”. Um recurso pode ser qualquer coisa: um usuário, um produto, um post de blog, um pedido de compra, uma foto. Cada recurso tem um endereço único (uma URL) e você pode interagir com esse recurso usando os métodos HTTP que já existem: GET para buscar, POST para criar, PUT para atualizar, DELETE para remover.

Por exemplo, imagine que você tem um blog e quer criar uma API REST para gerenciar os posts. Você poderia ter URLs assim:

  • GET /posts – retorna todos os posts
  • GET /posts/1 – retorna o post com ID 1
  • POST /posts – cria um novo post
  • PUT /posts/1 – atualiza o post com ID 1
  • DELETE /posts/1 – remove o post com ID 1

Percebe como fica intuitivo? A URL identifica qual recurso você quer acessar, e o método HTTP diz o que você quer fazer com ele. Não precisa ter URLs malucas tipo /getPosts, /createNewPost, /updatePostById. Tudo fica mais limpo e padronizado.

Outro ponto importante do REST é que ele é stateless, ou seja, sem estado. Isso significa que cada requisição que você faz para a API deve conter todas as informações necessárias para o servidor entender e processar aquela requisição. O servidor não “lembra” de requisições anteriores. Pode parecer estranho no começo, mas isso traz várias vantagens, como escalabilidade e simplicidade.

REST também promove a separação entre cliente e servidor. O frontend (cliente) não precisa saber como o backend (servidor) funciona internamente, e vice-versa. Eles só precisam concordar sobre o formato das mensagens que vão trocar. Isso significa que você pode mudar completamente seu frontend de React para Vue, por exemplo, sem precisar mexer em nada no backend.

Vale mencionar que nem toda API que usa HTTP é automaticamente uma API REST. Existem algumas regras e princípios que devem ser seguidos, e vamos explorar isso mais a fundo nos próximos tópicos. Mas o importante agora é você entender que REST é basicamente uma forma elegante e padronizada de criar APIs usando as ferramentas que a web já nos oferece.

Os Métodos HTTP e Como Usá-los

Agora vamos falar sobre os métodos HTTP, que são a espinha dorsal de qualquer API REST. Se você entender bem como cada método funciona, já vai estar uns 70% do caminho andado para dominar APIs REST.

Os métodos HTTP (também chamados de verbos HTTP) indicam qual ação você quer realizar em um determinado recurso. Os quatro principais métodos que você vai usar no dia a dia são: GET, POST, PUT e DELETE. Existem outros como PATCH, HEAD, OPTIONS, mas vamos focar nesses quatro primeiro porque são os mais importantes.

Vamos entender cada um deles:

GET é o método mais simples e mais usado. Ele serve para buscar informações. Quando você quer ler dados, consultar algo, listar recursos, você usa GET. É como abrir um livro para ler, você não está modificando nada, só consultando. Por exemplo, quando você acessa /usuarios/123 com GET, você está pedindo para ver as informações do usuário com ID 123. Uma característica importante do GET é que ele é idempotente, ou seja, você pode fazer a mesma requisição GET mil vezes e o resultado será sempre o mesmo. Além disso, requisições GET não devem ter efeitos colaterais, elas não devem modificar nada no servidor.

POST é usado para criar novos recursos. Quando você quer adicionar algo novo no sistema, seja um novo usuário, um novo produto, um novo comentário, você usa POST. Imagine que você está adicionando um novo contato na sua agenda telefônica. Você está criando algo que não existia antes. Por exemplo, se você faz um POST para /produtos com os dados de um produto novo no corpo da requisição, o servidor vai criar esse produto e geralmente retornar o produto criado com um ID gerado. Diferente do GET, o POST não é idempotente. Se você fizer a mesma requisição POST três vezes, vai criar três recursos diferentes (a não ser que você implemente alguma lógica para evitar duplicidade).

PUT serve para atualizar um recurso existente por completo. A ideia é que você está substituindo o recurso inteiro. Por exemplo, se você tem um usuário e quer atualizar todas as informações dele, você faz um PUT para /usuarios/123 enviando todos os dados do usuário, mesmo os que não mudaram. É como pegar uma folha de papel, amassar e jogar fora, e colocar uma folha nova no lugar. O PUT é idempotente, então se você fizer a mesma requisição PUT várias vezes, o resultado final será o mesmo.

DELETE é autoexplicativo, serve para remover recursos. Quando você quer excluir algo do sistema, usa DELETE. Por exemplo, DELETE /posts/456 vai remover o post com ID 456. Assim como PUT, DELETE também é idempotente. Se você tentar deletar o mesmo recurso várias vezes, depois da primeira vez ele já não vai mais existir, então as próximas tentativas vão apenas confirmar que o recurso não existe.

Existe também o PATCH, que é parecido com PUT mas com uma diferença importante: enquanto PUT substitui o recurso inteiro, PATCH faz uma atualização parcial. Então se você só quer mudar o email de um usuário sem mexer no resto, PATCH é mais apropriado. Você envia apenas os campos que quer modificar.

Vamos ver um exemplo prático. Imagina que você está construindo uma API para gerenciar tarefas (um to-do list). As requisições ficariam mais ou menos assim:

GET /tarefas - lista todas as tarefas
GET /tarefas/5 - busca a tarefa com ID 5
POST /tarefas - cria uma nova tarefa
PUT /tarefas/5 - atualiza completamente a tarefa 5
PATCH /tarefas/5 - atualiza parcialmente a tarefa 5
DELETE /tarefas/5 - remove a tarefa 5

Um erro comum que iniciantes cometem é usar o método errado para a operação. Por exemplo, usar GET para deletar algo (tipo GET /usuarios/delete/123) ou usar POST para tudo. Isso não só vai contra os princípios REST como também pode causar problemas de segurança e cache. Navegadores e proxies tratam métodos diferentes de formas diferentes. GET, por exemplo, pode ser cacheado, mas POST não.

Outra coisa importante: o corpo da requisição (request body) geralmente só é usado com POST, PUT e PATCH. Com GET e DELETE, você normalmente não envia dados no corpo, apenas na URL ou nos headers.

Códigos de Status HTTP

Quando você faz uma requisição para uma API REST, o servidor sempre te devolve um código de status junto com a resposta. Esses códigos são números de três dígitos que indicam se a requisição deu certo, se algo deu errado, ou se você precisa fazer alguma ação adicional. Entender esses códigos é fundamental para trabalhar com APIs.

Os códigos de status são divididos em cinco categorias, baseadas no primeiro dígito:

Códigos 1xx (Informacionais) são raros de ver no dia a dia. Eles indicam que a requisição foi recebida e o processo está continuando. Sinceramente, na maioria das APIs REST você nem vai usar esses códigos. O mais comum é o 100 Continue, mas é bem específico.

Códigos 2xx (Sucesso) indicam que deu tudo certo. Esses são os códigos felizes, os que você quer ver. O mais famoso é o 200 OK, que basicamente significa “sua requisição funcionou perfeitamente”. Quando você faz um GET e recebe os dados, geralmente vem com 200. Tem também o 201 Created, que é usado quando você cria algo novo com POST. É uma forma de dizer “o recurso foi criado com sucesso”. O 204 No Content é interessante porque indica sucesso mas sem conteúdo na resposta. É muito usado em DELETE, quando você remove algo e não precisa devolver nada, só confirmar que funcionou.

Códigos 3xx (Redirecionamento) indicam que o cliente precisa fazer algo mais para completar a requisição. O 301 Moved Permanently diz que o recurso mudou de endereço definitivamente. O 302 Found ou 307 Temporary Redirect indicam um redirecionamento temporário. O 304 Not Modified é usado para cache, dizendo que o recurso não mudou desde a última vez que você pediu.

Códigos 4xx (Erro do Cliente) são os erros da sua parte, ou seja, você fez algo errado na requisição. O mais famoso de todos é o 404 Not Found, que todo mundo já viu em algum momento. Significa que o recurso que você está procurando não existe. O 400 Bad Request indica que sua requisição está malformada, talvez você esqueceu algum campo obrigatório ou enviou dados no formato errado. O 401 Unauthorized significa que você precisa estar autenticado para acessar aquele recurso. Muita gente confunde com o 403 Forbidden, mas a diferença é sutil: 401 é “você não está logado”, enquanto 403 é “você está logado mas não tem permissão para isso”. O 405 Method Not Allowed aparece quando você usa o método HTTP errado, tipo fazer um POST em uma rota que só aceita GET. E o 409 Conflict indica algum conflito, como tentar criar um usuário com um email que já existe.

Códigos 5xx (Erro do Servidor) são os erros do lado do servidor. Quando você vê esses, geralmente não é culpa sua. O mais comum é o 500 Internal Server Error, que é o código genérico para “algo deu muito errado aqui no servidor mas não sei te dizer exatamente o quê”. O 502 Bad Gateway e 503 Service Unavailable geralmente aparecem quando o servidor está fora do ar ou sobrecarregado. O 504 Gateway Timeout indica que o servidor demorou demais para responder.

Usar os códigos certos é uma questão de boas práticas e comunicação clara. Quando você está desenvolvendo uma API, escolher o código de status apropriado ajuda os desenvolvedores que vão consumir sua API a entenderem o que aconteceu sem precisar ficar lendo mensagens de erro ou debugando.

Vamos ver um exemplo prático. Imagina que você está criando um endpoint para cadastrar usuários:

Se tudo der certo e o usuário for criado, você retorna 201 Created junto com os dados do usuário criado. Se o email já estiver em uso, você retorna 409 Conflict com uma mensagem explicando. Se o cliente esqueceu de enviar algum campo obrigatório, você retorna 400 Bad Request. Se o token de autenticação estiver inválido, você retorna 401 Unauthorized.

Um erro que vejo muito é API que retorna sempre 200 mesmo quando dá erro, e coloca a informação do erro só no corpo da resposta. Isso é uma péssima prática porque quebra a semântica do HTTP e dificulta o tratamento de erros no frontend.

Outra coisa importante: junto com o código de status, é uma boa prática sempre retornar uma mensagem descritiva no corpo da resposta, especialmente em casos de erro. Algo como:

{
  "erro": "Email já cadastrado",
  "detalhes": "O email [email protected] já está sendo usado por outro usuário",
  "codigo": "EMAIL_DUPLICADO"
}

Isso ajuda muito quem está consumindo sua API a entender exatamente o que aconteceu e como corrigir o problema.

Estrutura de uma Requisição e Resposta REST

Agora que você já conhece os métodos HTTP e os códigos de status, vamos entender como é a estrutura completa de uma requisição e de uma resposta em uma API REST. É importante conhecer todas as partes porque cada uma tem seu papel específico.

Uma requisição HTTP é composta por várias partes. Vamos dissecar cada uma delas:

A URL (ou endpoint) é o endereço para onde você está enviando a requisição. Por exemplo: https://api.exemplo.com/v1/usuarios/123. Ela geralmente contém o protocolo (https), o domínio (api.exemplo.com), a versão da API (v1), o recurso (usuarios) e, opcionalmente, um identificador específico (123).

O método HTTP já falamos bastante sobre ele no tópico anterior. É o verbo que indica a ação: GET, POST, PUT, DELETE, etc.

Os headers (cabeçalhos) são metadados sobre a requisição. Eles carregam informações importantes como o tipo de conteúdo que você está enviando, tokens de autenticação, idioma preferido, entre outros. Alguns headers comuns são:

  • Content-Type: indica o formato dos dados que você está enviando. Geralmente é application/json para APIs REST.
  • Accept: indica qual formato de resposta você aceita receber.
  • Authorization: carrega o token de autenticação, geralmente no formato “Bearer seu-token-aqui”.
  • User-Agent: identifica qual cliente está fazendo a requisição (navegador, app mobile, etc).

Query parameters (parâmetros de consulta) são aqueles valores que vêm depois do ponto de interrogação na URL. Por exemplo: /produtos?categoria=eletronicos&preco_max=1000. Eles são muito usados para filtrar, ordenar ou paginar resultados. São opcionais e geralmente usados com GET.

O body (corpo) é onde vão os dados da requisição, usado principalmente com POST, PUT e PATCH. É aqui que você envia as informações do recurso que quer criar ou atualizar. Geralmente é um JSON com os campos necessários.

Agora vamos ver a estrutura de uma resposta:

O status code já conhecemos bem. É aquele número de três dígitos que indica o resultado da operação.

Os response headers são os cabeçalhos da resposta, similares aos da requisição. Alguns importantes são:

  • Content-Type: indica o formato dos dados na resposta.
  • Content-Length: tamanho da resposta em bytes.
  • Date: quando a resposta foi gerada.
  • Server: informações sobre o servidor.

O response body é onde vêm os dados solicitados ou informações sobre o resultado da operação. Em APIs REST, geralmente é um JSON.

Vamos ver um exemplo completo de requisição e resposta:

Requisição:

POST /api/v1/usuarios
Host: exemplo.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{
  "nome": "Maria Silva",
  "email": "[email protected]",
  "senha": "senhaSegura123"
}

Resposta:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/usuarios/456
Date: Sat, 14 Feb 2026 10:30:00 GMT

{
  "id": 456,
  "nome": "Maria Silva",
  "email": "[email protected]",
  "criadoEm": "2026-02-14T10:30:00Z",
  "atualizadoEm": "2026-02-14T10:30:00Z"
}

Perceba que na resposta não retornamos a senha por questões de segurança. Também adicionamos campos extras como id e as datas de criação e atualização. O header Location indica onde o novo recurso pode ser acessado.

Outro exemplo, agora com GET e query parameters:

Requisição:

GET /api/v1/produtos?categoria=livros&ordenar=preco&limite=10
Host: exemplo.com
Accept: application/json

Resposta:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "total": 150,
  "pagina": 1,
  "itensPorPagina": 10,
  "dados": [
    {
      "id": 1,
      "nome": "Clean Code",
      "preco": 89.90,
      "categoria": "livros"
    },
    {
      "id": 2,
      "nome": "Código Limpo",
      "preco": 95.00,
      "categoria": "livros"
    }
  ]
}

Neste caso, além dos dados solicitados, incluímos metadados úteis como o total de itens disponíveis e informações de paginação.

Um detalhe importante: quando você está lidando com listas ou coleções, é uma boa prática envolver os dados em um objeto ao invés de retornar um array diretamente. Isso dá flexibilidade para adicionar metadados no futuro sem quebrar a compatibilidade.

Entender essa estrutura completa te ajuda a debugar problemas mais facilmente. Quando algo não funciona, você pode investigar cada parte: será que a URL está correta? Os headers estão sendo enviados? O body está no formato certo? O servidor está respondendo com o status adequado?

Os Princípios Fundamentais do REST

Agora que você já sabe como funcionam as requisições e respostas, vamos falar sobre os princípios que tornam uma API verdadeiramente RESTful. Roy Fielding definiu seis restrições arquiteturais na sua tese, e seguir esses princípios é o que separa uma API REST de verdade de uma API que só usa HTTP.

Cliente-Servidor (Client-Server) é o primeiro princípio. Ele determina que deve existir uma separação clara entre quem pede (cliente) e quem responde (servidor). O cliente não precisa saber como o servidor armazena os dados ou processa as requisições. O servidor não precisa saber como o cliente vai usar ou exibir os dados. Essa separação permite que ambos evoluam independentemente. Você pode reescrever todo seu frontend sem tocar no backend, e vice-versa. Isso traz flexibilidade enorme para o desenvolvimento.

Stateless (Sem Estado) é um dos princípios mais importantes e às vezes mal compreendido. Significa que cada requisição deve conter todas as informações necessárias para ser processada. O servidor não guarda nenhum contexto ou estado sobre o cliente entre requisições. Se você precisa estar autenticado, cada requisição deve enviar o token de autenticação. Se você está buscando a página 2 de uma lista, precisa informar isso na requisição. O servidor não “lembra” que você estava na página 1 antes.

Isso pode parecer ineficiente no começo, mas traz vantagens enormes. Primeiro, simplifica muito o servidor porque ele não precisa gerenciar sessões ou estados. Segundo, facilita a escalabilidade. Como cada requisição é independente, você pode distribuir as requisições entre vários servidores sem se preocupar. Terceiro, torna o sistema mais confiável porque se um servidor cair, outro pode assumir sem problemas.

Cache (Cacheable) determina que as respostas devem indicar explicitamente se podem ou não ser armazenadas em cache. Isso melhora muito a performance porque requisições idênticas não precisam ir até o servidor toda vez. Por exemplo, se você busca a lista de categorias de produtos, essa informação provavelmente não muda com frequência, então pode ser cacheada. O servidor indica isso através de headers como Cache-Control e ETag.

Interface Uniforme (Uniform Interface) é o que torna REST tão popular e fácil de entender. Esse princípio se desdobra em quatro subprincípios:

Primeiro, a identificação de recursos. Cada recurso deve ter um identificador único, geralmente uma URL. /usuarios/123 identifica o usuário 123.

Segundo, a manipulação de recursos através de representações. Quando você recebe um recurso, você recebe uma representação dele (geralmente JSON), e você pode modificar o recurso enviando uma nova representação.

Terceiro, mensagens autodescritivas. Cada requisição e resposta deve conter informações suficientes para entender como processá-la. Os headers Content-Type e Accept fazem parte disso.

Quarto, HATEOAS (Hypermedia as the Engine of Application State). Esse é o mais complexo e menos implementado. A ideia é que as respostas devem incluir links para ações possíveis. Por exemplo, ao buscar um pedido, a resposta incluiria links para cancelar o pedido, ver os detalhes de pagamento, etc. Na prática, muitas APIs REST não implementam HATEOAS completamente e ainda assim são consideradas RESTful.

Sistema em Camadas (Layered System) significa que a arquitetura pode ser composta por camadas hierárquicas, e cada componente não pode ver além da camada com a qual está interagindo. Você pode ter load balancers, caches, proxies, gateways entre o cliente e o servidor final, e o cliente nem precisa saber. Isso permite adicionar segurança, balanceamento de carga e cache de forma transparente.

Código Sob Demanda (Code on Demand) é o único princípio opcional. Permite que o servidor envie código executável para o cliente quando necessário, como JavaScript. Mas isso é raro em APIs REST modernas.

Agora, aqui vai uma verdade: a maioria das APIs que se dizem REST na verdade não seguem todos esses princípios rigorosamente. Muitas ignoram o HATEOAS, por exemplo. E está tudo bem. O importante é entender os princípios e aplicar aqueles que fazem sentido para seu caso. Uma API que segue a maioria desses princípios já traz os benefícios principais: simplicidade, escalabilidade, separação de responsabilidades e facilidade de manutenção.

O termo “RESTful” geralmente é usado para APIs que seguem a maioria desses princípios de forma pragmática. Já o termo “REST puro” seria para APIs que seguem absolutamente tudo, incluindo HATEOAS, mas essas são bem mais raras no mundo real.

O mais importante é você entender o porquê desses princípios. Eles não são regras arbitrárias, cada um resolve problemas específicos de arquitetura de sistemas distribuídos. Quando você entende o “porquê”, fica mais fácil decidir quando seguir estritamente e quando fazer concessões práticas.

URLs e Recursos: Como Nomear Corretamente

Uma das coisas que mais causam dúvida quando você está começando a criar APIs REST é: como eu devo nomear minhas URLs? Qual a forma certa de estruturar os endpoints? E olha, não existe uma resposta única e definitiva, mas existem convenções e boas práticas que a comunidade adotou ao longo dos anos.

A primeira regra de ouro é: use substantivos, não verbos. Lembra que falamos sobre recursos? Pois então, suas URLs devem representar recursos (coisas), e os métodos HTTP representam as ações. Então ao invés de criar URLs como /criarUsuario, /obterUsuario, /atualizarUsuario, você simplesmente usa /usuarios e deixa o método HTTP indicar a ação.

Use o plural para coleções. Existe um debate eterno sobre usar singular ou plural, mas a convenção mais aceita é usar plural. Então /usuarios ao invés de /usuario, /produtos ao invés de /produto. Isso fica mais intuitivo quando você pensa que GET /usuarios retorna vários usuários, e POST /usuarios adiciona um usuário à coleção de usuários.

Mantenha as URLs simples e intuitivas. Alguém deveria conseguir olhar para sua URL e entender mais ou menos o que ela faz. Compare /api/v1/usuarios/123/pedidos com /api/v1/obterPedidosDoUsuario?id=123. A primeira é muito mais limpa e óbvia.

Use hierarquias para expressar relacionamentos. Se um recurso pertence a outro, mostre isso na URL. Por exemplo, se você quer buscar os comentários de um post específico, use /posts/456/comentarios. Se quer um comentário específico desse post, use /posts/456/comentarios/789. Isso deixa claro que comentários pertencem a posts.

Mas cuidado para não exagerar na profundidade. URLs com mais de 3 níveis começam a ficar confusas. Se você se pegar fazendo algo como /usuarios/123/pedidos/456/itens/789/avaliacoes, provavelmente está na hora de repensar sua estrutura.

Use hífens para separar palavras, não underscores. Então /categorias-produtos ao invés de /categorias_produtos. Isso é mais uma questão de convenção, mas hífens são mais legíveis em URLs e são o padrão mais aceito.

Tudo em minúsculas. URLs devem ser case-insensitive na prática, mas a convenção é usar tudo minúsculo. Então /usuarios, não /Usuarios ou /USUARIOS.

Evite extensões de arquivo. Não use coisas como /usuarios.json ou /produtos.xml. O formato da resposta deve ser controlado pelo header Accept da requisição e pelo header Content-Type da resposta, não pela URL.

Use query parameters para filtros, ordenação e paginação. Essas coisas não são parte da identidade do recurso, então não devem estar no caminho da URL. Use algo como /produtos?categoria=eletronicos&ordenar=preco&pagina=2.

Vamos ver alguns exemplos de URLs bem estruturadas:

GET /usuarios - lista todos os usuários
GET /usuarios/123 - busca o usuário 123
POST /usuarios - cria um novo usuário
PUT /usuarios/123 - atualiza o usuário 123
DELETE /usuarios/123 - remove o usuário 123

GET /usuarios/123/pedidos - lista pedidos do usuário 123
GET /pedidos/456 - busca o pedido 456
GET /pedidos/456/itens - lista itens do pedido 456

GET /produtos?categoria=livros&preco_max=100
GET /posts?autor=maria&ordenar=data&limite=20

Agora vamos ver exemplos do que evitar:

❌ /getUsuarios - não use verbos
❌ /usuario/criar - não use verbos
❌ /usuarios/deletar/123 - não use verbos
❌ /Usuarios - não use maiúsculas
❌ /usuarios_ativos - prefira hífens
❌ /usuarios.json - não use extensões

Versionamento na URL é outro ponto importante. É comum incluir a versão da API na URL, tipo /v1/usuarios ou /api/v1/usuarios. Isso permite que você faça mudanças significativas na API sem quebrar clientes que ainda usam a versão antiga. Vamos falar mais sobre versionamento em um tópico específico mais adiante.

Ações especiais às vezes são necessárias e não se encaixam perfeitamente no modelo CRUD (Create, Read, Update, Delete). Por exemplo, se você tem um endpoint para ativar uma conta de usuário, você poderia fazer POST /usuarios/123/ativar ou PATCH /usuarios/123 com { “status”: “ativo” }. Ambas as abordagens são válidas, use o que fizer mais sentido no seu contexto.

Uma dica importante: documente suas URLs. Por melhor que seja seu design de API, sempre haverá casos que precisam de explicação. Use ferramentas como Swagger (vamos falar disso mais tarde) para documentar cada endpoint, seus parâmetros, formatos de requisição e resposta.

E lembre-se: consistência é mais importante do que perfeição. Se você decidir usar uma convenção diferente em algum ponto, tudo bem, mas mantenha essa convenção em toda a API. Nada é mais confuso do que uma API onde metade dos endpoints segue um padrão e a outra metade segue outro.

Exemplo Prático: Criando uma API REST com NestJS

Agora que já cobrimos toda a teoria, vamos colocar a mão na massa e ver como isso funciona na prática. Vou usar NestJS nos exemplos, mas quero reforçar novamente: os conceitos que vamos aplicar aqui funcionam em qualquer framework ou linguagem. Você pode seguir a mesma lógica em Express, FastAPI, Django, Spring Boot, Laravel, ou qualquer outra ferramenta.

NestJS é um framework Node.js que usa TypeScript e tem uma arquitetura muito bem organizada, inspirada no Angular. Ele já vem com várias ferramentas prontas que facilitam a criação de APIs REST robustas e escaláveis.

Vamos criar uma API simples para gerenciar livros. Primeiro, você precisa ter o Node.js instalado. Depois, instale o CLI do NestJS:

npm install -g @nestjs/cli
nest new biblioteca-api

Isso cria um projeto novo. Agora vamos criar um módulo para gerenciar livros:

nest generate resource livros

Esse comando cria automaticamente o controller, service, module e até os DTOs (Data Transfer Objects) básicos. Vamos começar definindo como um livro se parece. No arquivo create-livro.dto.ts:

export class CreateLivroDto {
  titulo: string;
  autor: string;
  anoPublicacao: number;
  isbn: string;
  preco: number;
}

DTO é basicamente a estrutura de dados que você espera receber nas requisições. Agora vamos ao controller, que é onde definimos os endpoints. No arquivo livros.controller.ts:

import { Controller, Get, Post, Put, Delete, Body, Param, HttpCode, HttpStatus } from '@nestjs/common';
import { LivrosService } from './livros.service';
import { CreateLivroDto } from './dto/create-livro.dto';

@Controller('livros')
export class LivrosController {
  constructor(private readonly livrosService: LivrosService) {}

  @Get()
  listarTodos() {
    return this.livrosService.listarTodos();
  }

  @Get(':id')
  buscarPorId(@Param('id') id: string) {
    return this.livrosService.buscarPorId(id);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  criar(@Body() createLivroDto: CreateLivroDto) {
    return this.livrosService.criar(createLivroDto);
  }

  @Put(':id')
  atualizar(@Param('id') id: string, @Body() updateLivroDto: CreateLivroDto) {
    return this.livrosService.atualizar(id, updateLivroDto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remover(@Param('id') id: string) {
    return this.livrosService.remover(id);
  }
}

Veja como fica intuitivo. O decorator @Controller('livros') define que todos os endpoints desse controller começam com /livros. Os decorators @Get(), @Post(), etc, definem qual método HTTP cada função responde. O @Param('id') captura o ID da URL, e o @Body() pega os dados enviados no corpo da requisição.

Repare também que estamos usando os códigos de status corretos. @HttpCode(HttpStatus.CREATED) para POST (201) e @HttpCode(HttpStatus.NO_CONTENT) para DELETE (204).

Agora vamos ao service, onde fica a lógica de negócio. No arquivo livros.service.ts:

import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateLivroDto } from './dto/create-livro.dto';

@Injectable()
export class LivrosService {
  private livros = [];
  private proximoId = 1;

  listarTodos() {
    return this.livros;
  }

  buscarPorId(id: string) {
    const livro = this.livros.find(l => l.id === parseInt(id));
    if (!livro) {
      throw new NotFoundException(`Livro com ID ${id} não encontrado`);
    }
    return livro;
  }

  criar(createLivroDto: CreateLivroDto) {
    const novoLivro = {
      id: this.proximoId++,
      ...createLivroDto,
      criadoEm: new Date().toISOString()
    };
    this.livros.push(novoLivro);
    return novoLivro;
  }

  atualizar(id: string, updateLivroDto: CreateLivroDto) {
    const index = this.livros.findIndex(l => l.id === parseInt(id));
    if (index === -1) {
      throw new NotFoundException(`Livro com ID ${id} não encontrado`);
    }
    this.livros[index] = {
      ...this.livros[index],
      ...updateLivroDto,
      atualizadoEm: new Date().toISOString()
    };
    return this.livros[index];
  }

  remover(id: string) {
    const index = this.livros.findIndex(l => l.id === parseInt(id));
    if (index === -1) {
      throw new NotFoundException(`Livro com ID ${id} não encontrado`);
    }
    this.livros.splice(index, 1);
  }
}

Neste exemplo simples estamos guardando os livros em memória num array. Na vida real você usaria um banco de dados, mas a lógica seria parecida. Veja que quando um livro não é encontrado, lançamos uma NotFoundException, que o NestJS automaticamente converte em uma resposta 404.

Agora você pode rodar a aplicação:

npm run start:dev

E testar os endpoints:

# Criar um livro
POST http://localhost:3000/livros
{
  "titulo": "Clean Code",
  "autor": "Robert Martin",
  "anoPublicacao": 2008,
  "isbn": "978-0132350884",
  "preco": 89.90
}

# Listar todos
GET http://localhost:3000/livros

# Buscar um específico
GET http://localhost:3000/livros/1

# Atualizar
PUT http://localhost:3000/livros/1
{
  "titulo": "Clean Code",
  "autor": "Robert C. Martin",
  "anoPublicacao": 2008,
  "isbn": "978-0132350884",
  "preco": 79.90
}

# Deletar
DELETE http://localhost:3000/livros/1

Uma coisa legal do NestJS é que ele já vem com validação embutida. Você pode adicionar decorators nos DTOs para validar os dados:

import { IsString, IsNumber, IsISBN, Min } from 'class-validator';

export class CreateLivroDto {
  @IsString()
  titulo: string;

  @IsString()
  autor: string;

  @IsNumber()
  @Min(1000)
  anoPublicacao: number;

  @IsISBN()
  isbn: string;

  @IsNumber()
  @Min(0)
  preco: number;
}

Com isso, se alguém tentar criar um livro sem título ou com preço negativo, a API automaticamente retorna um erro 400 com detalhes sobre o que está errado.

Mais uma vez: você não precisa usar NestJS. Em Express puro seria assim:

const express = require('express');
const app = express();
app.use(express.json());

let livros = [];
let proximoId = 1;

app.get('/livros', (req, res) => {
  res.json(livros);
});

app.post('/livros', (req, res) => {
  const novoLivro = {
    id: proximoId++,
    ...req.body
  };
  livros.push(novoLivro);
  res.status(201).json(novoLivro);
});

app.listen(3000);

A lógica é a mesma, só muda a sintaxe e a organização. O importante é você entender os conceitos REST que estamos aplicando: usar os métodos HTTP corretos, retornar os status codes apropriados, estruturar bem as URLs, e trabalhar com JSON.

Autenticação e Segurança em APIs REST

Até agora criamos uma API funcional, mas ela tem um problema sério: qualquer pessoa pode acessar, criar, modificar ou deletar dados. No mundo real, você precisa controlar quem pode fazer o quê na sua API. É aqui que entram autenticação e autorização.

Primeiro, vamos entender a diferença entre esses dois conceitos que muita gente confunde. Autenticação é verificar quem você é (tipo mostrar sua identidade). Autorização é verificar o que você pode fazer (tipo mostrar sua permissão). Você pode estar autenticado mas não autorizado a fazer algo específico.

Existem várias formas de implementar autenticação em APIs REST. Vamos ver as mais comuns:

API Keys são a forma mais simples. Você gera uma chave única para cada cliente e ele envia essa chave em cada requisição, geralmente no header. Por exemplo:

GET /livros
Authorization: ApiKey sua-chave-aqui-123456

É simples de implementar mas tem limitações. A chave não expira automaticamente, não carrega informações sobre o usuário, e se alguém rouba a chave, tem acesso total até você revogar manualmente.

Basic Authentication usa usuário e senha codificados em Base64. A cada requisição você envia algo assim:

Authorization: Basic dXN1YXJpbzpzZW5oYQ==

Onde aquela string é apenas “usuario:senha” codificado. O problema é que Base64 não é criptografia, é só codificação. Qualquer um pode decodificar. Por isso, Basic Auth só deve ser usado com HTTPS. Mesmo assim, enviar credenciais em cada requisição não é ideal.

JWT (JSON Web Tokens) é provavelmente o método mais popular hoje em dia para APIs REST. A ideia é: você faz login uma vez com usuário e senha, e o servidor te devolve um token JWT. Esse token contém informações sobre você (como seu ID e permissões) de forma assinada. Nas próximas requisições, você envia esse token ao invés de usuário e senha.

Um JWT tem três partes separadas por pontos:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywibm9tZSI6Ik1hcmlhIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

A primeira parte é o header (tipo de token e algoritmo), a segunda é o payload (os dados do usuário), e a terceira é a assinatura (que garante que o token não foi adulterado).

Vamos ver como implementar JWT no NestJS. Primeiro instale as dependências:

npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt

Crie um módulo de autenticação:

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}

  async login(email: string, senha: string) {
    // Aqui você buscaria o usuário no banco
    const usuario = await this.buscarUsuarioPorEmail(email);
    
    if (!usuario) {
      throw new UnauthorizedException('Credenciais inválidas');
    }

    // Compara a senha com o hash armazenado
    const senhaValida = await bcrypt.compare(senha, usuario.senhaHash);
    
    if (!senhaValida) {
      throw new UnauthorizedException('Credenciais inválidas');
    }

    // Gera o token JWT
    const payload = { 
      sub: usuario.id, 
      email: usuario.email,
      role: usuario.role 
    };
    
    return {
      accessToken: this.jwtService.sign(payload),
      usuario: {
        id: usuario.id,
        nome: usuario.nome,
        email: usuario.email
      }
    };
  }

  async validarToken(token: string) {
    try {
      return this.jwtService.verify(token);
    } catch (erro) {
      throw new UnauthorizedException('Token inválido');
    }
  }
}

Agora você cria um guard (proteção) que verifica o token em cada requisição protegida:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = this.extrairToken(request);

    if (!token) {
      throw new UnauthorizedException('Token não fornecido');
    }

    try {
      const payload = this.jwtService.verify(token);
      request.user = payload; // Adiciona os dados do usuário na requisição
      return true;
    } catch {
      throw new UnauthorizedException('Token inválido ou expirado');
    }
  }

  private extrairToken(request: any): string | undefined {
    const authorization = request.headers.authorization;
    if (!authorization) return undefined;
    
    const [tipo, token] = authorization.split(' ');
    return tipo === 'Bearer' ? token : undefined;
  }
}

E usa o guard nos seus controllers:

@Controller('livros')
export class LivrosController {
  @Get()
  @UseGuards(JwtAuthGuard) // Protege esta rota
  listarTodos() {
    return this.livrosService.listarTodos();
  }

  @Post()
  @UseGuards(JwtAuthGuard)
  criar(@Body() createLivroDto: CreateLivroDto, @Request() req) {
    // req.user contém os dados do token JWT
    console.log('Usuário autenticado:', req.user);
    return this.livrosService.criar(createLivroDto);
  }
}
```

Agora para acessar esses endpoints, o cliente precisa primeiro fazer login e depois enviar o token:
```
POST /auth/login
{
  "email": "[email protected]",
  "senha": "senhaSegura123"
}

Resposta:
{
  "accessToken": "eyJhbGci...",
  "usuario": { ... }
}

Depois:
GET /livros
Authorization: Bearer eyJhbGci...

Algumas boas práticas de segurança importantes:

Sempre use HTTPS em produção. Sem isso, tokens e dados podem ser interceptados.

Nunca armazene senhas em texto puro. Sempre use hashing com bcrypt, argon2 ou similar.

Tokens devem expirar. Configure um tempo de vida curto (tipo 15 minutos a 1 hora) e implemente refresh tokens para renovar.

Valide tudo que vem do cliente. Nunca confie nos dados de entrada. Use bibliotecas como class-validator.

Implemente rate limiting para prevenir ataques de força bruta. Limite quantas requisições um IP pode fazer por minuto.

Use CORS apropriadamente. Configure quais domínios podem acessar sua API.

app.enableCors({
  origin: ['https://seusite.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true
});

Sanitize inputs para prevenir SQL Injection e XSS. ORMs como TypeORM já ajudam com isso, mas sempre valide.

Não exponha informações sensíveis nas mensagens de erro. “Credenciais inválidas” é melhor que “Senha incorreta” porque o segundo confirma que o email existe.

Segurança é um tópico vasto e em constante evolução. O importante é você começar com esses fundamentos e ir aprofundando conforme necessário. Uma API insegura pode comprometer todo seu sistema e seus usuários.

Versionamento de APIs

Imagine a situação: você tem uma API em produção, sendo usada por vários clientes (apps mobile, sites, sistemas parceiros). De repente você precisa fazer uma mudança significativa na estrutura de dados ou na forma como um endpoint funciona. Se você simplesmente mudar, todos os clientes que dependem da versão antiga vão quebrar. É aí que entra o versionamento de APIs.

Versionamento permite que você evolua sua API sem quebrar compatibilidade com clientes que ainda usam versões antigas. Você mantém a versão antiga funcionando enquanto oferece uma nova versão com as mudanças. Os clientes podem migrar no tempo deles, sem pressa.

Quando você deve criar uma nova versão?

Nem toda mudança exige uma nova versão. Adicionar um campo novo opcional em uma resposta, por exemplo, geralmente não quebra nada. Mas mudanças que quebram compatibilidade (breaking changes) exigem nova versão. Exemplos:

  • Remover ou renomear um campo
  • Mudar o tipo de um campo (de string para número, por exemplo)
  • Mudar a estrutura de um objeto
  • Remover um endpoint
  • Mudar o comportamento esperado de forma significativa
  • Tornar um campo opcional em obrigatório

Mudanças que não quebram compatibilidade (não exigem nova versão):

  • Adicionar novos campos opcionais
  • Adicionar novos endpoints
  • Adicionar novos parâmetros opcionais
  • Tornar um campo obrigatório em opcional
  • Correções de bugs que não mudam o comportamento esperado

Formas de versionar APIs:

Existem várias estratégias de versionamento, cada uma com prós e contras. Vamos ver as mais comuns:

Versionamento na URL é a forma mais popular e visível. Você coloca a versão diretamente no caminho da URL:

/v1/livros
/v2/livros
/v1/usuarios
/v2/usuarios

Ou às vezes:

/api/v1/livros
/api/v2/livros

Vantagens: é super claro qual versão está sendo usada, fácil de rotear, fácil de cachear, fácil de testar. Desvantagens: URLs diferentes para o mesmo recurso conceitualmente, pode ficar feio na documentação.

Versionamento no header mantém a URL limpa e coloca a versão em um header customizado:

GET /livros
Accept-Version: v2

Ou usando o header Accept padrão:

GET /livros
Accept: application/vnd.minhaapi.v2+json

Vantagens: URLs limpas e consistentes, segue mais fielmente os princípios REST. Desvantagens: menos óbvio, mais difícil de testar manualmente, problemas com cache se não configurado corretamente.

Versionamento por query parameter coloca a versão como parâmetro na URL:

/livros?version=2

Vantagens: simples de implementar. Desvantagens: pode confundir com outros parâmetros, não é semântico, problemas com cache.

Versionamento por subdomínio usa domínios diferentes:

v1.api.exemplo.com/livros
v2.api.exemplo.com/livros

Vantagens: isolamento total entre versões, pode até ter infraestrutura separada. Desvantagens: mais complexo de manter, certificados SSL para cada subdomínio.

A estratégia mais popular e recomendada é o versionamento na URL. É simples, explícita, e amplamente usada por grandes empresas (Stripe, Twitter, GitHub, etc).

Como implementar versionamento no NestJS:

No NestJS é bem simples. Você pode habilitar versionamento global:

// main.ts
import { VersioningType } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '1'
  });
  
  await app.listen(3000);
}

E então especificar a versão em cada controller:

@Controller({
  path: 'livros',
  version: '1'
})
export class LivrosV1Controller {
  @Get()
  listarTodos() {
    return { mensagem: 'Versão 1 da API' };
  }
}

@Controller({
  path: 'livros',
  version: '2'
})
export class LivrosV2Controller {
  @Get()
  listarTodos() {
    return { mensagem: 'Versão 2 da API com melhorias' };
  }
}
```

Agora você tem:
```
GET /v1/livros - usa LivrosV1Controller
GET /v2/livros - usa LivrosV2Controller

Boas práticas de versionamento:

Use versionamento semântico simplificado. Normalmente APIs usam apenas o número major (v1, v2, v3). Não precisa v1.2.3, isso é para bibliotecas.

Documente claramente o que mudou entre versões. Mantenha um changelog mostrando o que foi adicionado, removido ou modificado.

Deprecie versões antigas gradualmente. Não remova uma versão de uma hora para outra. Anuncie com antecedência (3 meses, 6 meses), marque como deprecated na documentação, avise os clientes, e só então remova.

Não tenha muitas versões ativas simultaneamente. Idealmente mantenha no máximo 2-3 versões. Cada versão adicional é código extra para manter, testar e monitorar.

Compartilhe código entre versões quando possível. Se 90% da lógica é igual, não duplique tudo. Use herança, composição, ou serviços compartilhados.

// Service compartilhado
@Injectable()
export class LivrosService {
  async buscarLivros() {
    // lógica comum
  }
}

// Controllers específicos por versão
@Controller({ path: 'livros', version: '1' })
export class LivrosV1Controller {
  constructor(private livrosService: LivrosService) {}
  
  @Get()
  async listar() {
    const livros = await this.livrosService.buscarLivros();
    return this.formatarV1(livros); // formato específico v1
  }
}

@Controller({ path: 'livros', version: '2' })
export class LivrosV2Controller {
  constructor(private livrosService: LivrosService) {}
  
  @Get()
  async listar() {
    const livros = await this.livrosService.buscarLivros();
    return this.formatarV2(livros); // formato específico v2
  }
}

Considere sunset headers para avisar quando uma versão vai ser descontinuada:

@Header('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT')
@Header('Link', '<https://api.exemplo.com/v2/livros>; rel="successor-version"')

Por último, lembre-se: versionamento adiciona complexidade. Evite criar novas versões sem necessidade real. Às vezes é melhor fazer mudanças retrocompatíveis ou usar feature flags do que criar uma versão inteira nova. Mas quando uma breaking change é realmente necessária, ter um sistema de versionamento bem implementado salva você de muitas dores de cabeça.

Boas Práticas e Erros Comuns

Depois de tudo que vimos até aqui, vamos consolidar algumas boas práticas e falar sobre erros que praticamente todo desenvolvedor comete quando está começando com APIs REST. Eu mesmo já cometi vários desses erros, então não se sinta mal se reconhecer alguns deles no seu código.

Erro 1: Usar o método HTTP errado

Esse é clássico. Iniciantes às vezes usam POST para tudo, ou pior, usam GET para operações que modificam dados. Lembra: GET é só para leitura, POST para criar, PUT/PATCH para atualizar, DELETE para remover. Usar GET para deletar algo (tipo GET /usuarios/delete/123) não só vai contra REST como pode causar problemas sérios, já que navegadores e crawlers fazem requisições GET automaticamente.

Erro 2: Não usar os códigos de status corretos

Outra coisa muito comum é retornar sempre 200 OK, mesmo quando dá erro, e colocar a informação do erro só no corpo da resposta. Isso quebra a semântica do HTTP. Use 201 para criação, 204 para deleção sem conteúdo, 400 para erro de validação, 401 para falta de autenticação, 404 para não encontrado, 500 para erro interno.

Erro 3: Nomear URLs com verbos

URLs como /criarUsuario, /obterProdutos, /atualizarPedido são ruins. Use substantivos (recursos) e deixe o método HTTP indicar a ação. Então /usuarios com POST para criar, GET para listar, etc.

Erro 4: Não validar dados de entrada

Nunca confie nos dados que vêm do cliente. Sempre valide tudo. Use bibliotecas como class-validator no NestJS ou Joi em Node puro. Verifique tipos, tamanhos, formatos, valores permitidos. Um campo que espera um número não deve aceitar uma string maliciosa.

Erro 5: Expor IDs internos desnecessariamente

Às vezes você tem IDs sequenciais (1, 2, 3, 4…) que revelam informações sobre o volume de dados. Em alguns casos pode ser melhor usar UUIDs, especialmente em recursos sensíveis. Mas isso depende do contexto, IDs sequenciais não são sempre um problema.

Erro 6: Não implementar paginação

Se você tem um endpoint que retorna uma lista, sempre implemente paginação. Retornar 10.000 registros de uma vez vai travar o cliente e desperdiçar banda. Use algo como:

GET /livros?pagina=1&limite=20

E retorne metadados úteis:

{
  "dados": [...],
  "paginacao": {
    "paginaAtual": 1,
    "totalPaginas": 50,
    "totalItens": 1000,
    "itensPorPagina": 20
  }
}

Erro 7: Não tratar erros adequadamente

Simplesmente deixar a aplicação crashar quando algo dá errado não é opção. Implemente tratamento de erros global. No NestJS você pode usar Exception Filters:

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus?.() || 500;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      mensagem: exception.message || 'Erro interno do servidor'
    });
  }
}

Erro 8: Não usar HTTPS em produção

APIs REST frequentemente trafegam dados sensíveis (senhas, tokens, dados pessoais). Sem HTTPS, tudo isso vai em texto puro pela rede. Sempre use HTTPS em produção. Hoje em dia com Let’s Encrypt é até gratuito.

Erro 9: Não implementar rate limiting

Sem rate limiting, sua API fica vulnerável a ataques de negação de serviço (DoS) e abuso. Limite quantas requisições um IP ou usuário pode fazer em determinado período:

import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60, // 60 segundos
      limit: 10, // 10 requisições
    }),
  ],
})
export class AppModule {}

Erro 10: Retornar dados sensíveis

Nunca retorne senhas, nem mesmo hasheadas. Não retorne tokens de sessão de outros usuários. Seja cuidadoso com dados pessoais. Sempre filtre o que vai na resposta:

// Ruim
return usuario; // inclui senha, tokens internos, etc

// Bom
return {
  id: usuario.id,
  nome: usuario.nome,
  email: usuario.email,
  criadoEm: usuario.criadoEm
};

Erro 11: Inconsistência nos nomes

Se você usa camelCase em um endpoint, use em todos. Se usa snake_case, use em todos. Se pluraliza recursos (/usuarios), pluralize todos. Consistência é fundamental.

Boas práticas que você deve seguir:

Use versionamento desde o início. Mesmo que seja só v1, já comece com isso. Facilita muito quando precisar fazer mudanças futuramente.

Implemente logs adequados. Registre requisições importantes, erros, tentativas de acesso não autorizado. Isso ajuda muito no debug e monitoramento.

Use variáveis de ambiente para configurações sensíveis. Nunca commite senhas, tokens, chaves de API no código. Use arquivos .env e adicione .env ao .gitignore.

// .env
DATABASE_URL=postgresql://...
JWT_SECRET=sua-chave-secreta-aqui
PORT=3000

Implemente health checks. Um endpoint simples que verifica se a API está funcionando:

@Get('health')
@ApiOperation({ summary: 'Verifica saúde da API' })
health() {
  return {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  };
}

Use DTOs para entrada e saída. Não exponha suas entidades de banco diretamente. DTOs dão controle sobre exatamente quais campos entram e saem.

Implemente CORS adequadamente se sua API vai ser consumida por frontends em domínios diferentes:

app.enableCors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true,
});

Teste sua API. Escreva testes unitários para a lógica de negócio e testes de integração para os endpoints. Isso evita regressões quando você faz mudanças.

Monitore performance. Endpoints muito lentos prejudicam a experiência. Use ferramentas como New Relic, DataDog, ou até logs simples para identificar gargalos.

Mantenha suas dependências atualizadas. Vulnerabilidades de segurança são descobertas o tempo todo. Use comandos como npm audit regularmente.

Por fim, lembre-se que boas práticas são guias, não leis absolutas. Às vezes você vai precisar fazer concessões por questões práticas. O importante é entender o porquê de cada prática e fazer escolhas conscientes. Com o tempo e experiência, muitas dessas coisas vão se tornar naturais no seu desenvolvimento.

Conclusão

E aí, conseguiu acompanhar tudo até aqui? Se sim, parabéns! Você acabou de dar um passo gigante no entendimento de APIs REST. Sei que foi bastante conteúdo, mas acredite, agora você tem uma base sólida para começar a criar suas próprias APIs ou consumir APIs de terceiros com muito mais confiança.

Vamos recapitular rapidamente o que vimos neste guia. Começamos entendendo o que é uma API e por que elas são tão importantes no desenvolvimento moderno. Depois mergulhamos no conceito de REST, esse estilo arquitetural que se tornou praticamente o padrão da indústria para criação de APIs web.

Exploramos os métodos HTTP (GET, POST, PUT, DELETE) e como cada um tem seu papel específico. Vimos os códigos de status e como eles comunicam o resultado de cada operação de forma clara e padronizada. Entendemos a estrutura completa de requisições e respostas, com seus headers, body, query parameters e tudo mais.

Falamos sobre os princípios fundamentais que tornam uma API verdadeiramente RESTful, mesmo que na prática muitas APIs façam algumas concessões. Aprendemos a nomear URLs de forma intuitiva e consistente, usando substantivos e hierarquias que fazem sentido.

JSON se mostrou como o formato preferido para troca de dados, e vimos boas práticas para trabalhar com ele. Depois colocamos a mão na massa com um exemplo prático usando NestJS, mas sempre lembrando que os conceitos se aplicam a qualquer framework ou linguagem.

Segurança foi um tópico importante, onde falamos sobre autenticação, autorização, JWT, e várias práticas para manter sua API segura.

Versionamento nos mostrou como evoluir uma API sem quebrar clientes existentes.

O mais legal de tudo isso é que APIs REST não são rocket science. Sim, tem complexidade quando você vai mais fundo, mas os fundamentos são acessíveis e práticos. Você não precisa dominar tudo de primeira. Comece simples, crie uma API básica, teste, quebre coisas, conserte, e vá evoluindo.

Uma coisa importante: não fique preso à perfeição. Sua primeira API não vai seguir todas as boas práticas, e está tudo bem. O importante é começar, aprender com a experiência, e ir melhorando. Eu garanto que daqui seis meses, quando você olhar para o código que escreveu hoje, vai pensar “nossa, eu poderia ter feito muito melhor”. E isso é excelente, porque significa que você evoluiu.

APIs REST são uma habilidade extremamente valiosa no mercado. Praticamente toda aplicação moderna precisa de uma API, seja para servir um app mobile, um site, integrar com sistemas parceiros, ou permitir automações. Dominar esse assunto abre muitas portas profissionalmente.

Continue praticando. Crie projetos pessoais, contribua com projetos open source, estude APIs públicas de empresas grandes (Stripe, GitHub, Twitter) para ver como elas estruturam as coisas. Leia documentações, acompanhe discussões sobre design de APIs, mantenha-se atualizado com as tendências.

E lembra: a comunidade de desenvolvimento é enorme e acolhedora. Quando tiver dúvidas, não hesite em perguntar. Participe de fóruns, grupos no Discord, Stack Overflow, Reddit. Todo mundo já foi iniciante um dia, e a maioria das pessoas fica feliz em ajudar.

Agora é com você. Pegue tudo que aprendeu aqui e coloque em prática. Comece pequeno, talvez uma API para gerenciar suas tarefas pessoais, ou para catalogar seus livros, ou qualquer coisa que faça sentido para você. O importante é tirar do papel e começar a escrever código.

Boa sorte na sua jornada com APIs REST, e que você construa coisas incríveis!

E você, já trabalhou com APIs REST antes? Qual foi sua maior dificuldade ao começar? Ou se está começando agora, qual parte deste guia foi mais útil para você? Compartilhe sua experiência nos comentários! Sua história pode ajudar outros desenvolvedores que estão na mesma jornada. Vamos trocar ideias e aprender juntos!