REST API: como evitar erros comuns e aplicar boas práticas

Rocketseat

Navegação Rápida:
Construir uma REST API robusta e escalável é uma habilidade essencial para desenvolvedores(as) de todos os níveis. No dia a dia, é comum vermos erros comuns em REST APIs que podem comprometer a manutenibilidade, segurança e performance de um projeto. Mas não se preocupe: este artigo vai guiá-lo(a) pelas boas práticas de REST API no contexto de Node.js com TypeScript, mostrando como evitar armadilhas típicas no design de API e adotar padrões que facilitam a vida de todos, desde quem desenvolve até quem consome a API.
Vamos abordar de forma direta vários cenários de problema → solução. Em cada seção, apresentamos um problema real (um erro de design ou implementação de APIs REST) e em seguida a solução recomendada (a boa prática correspondente), incluindo exemplos práticos com Node.js (usando frameworks populares como Express ou Fastify) e TypeScript.
Antes de começarmos, vale lembrar: seguir os padrões REST (como manter sua API stateless, usar as convenções de endpoints REST, etc.) não é apenas questão de estilo, mas de construir sistemas mais fáceis de usar e manter. Então, bora codar?
Estrutura e nomenclatura de endpoints
Problema: endpoints desorganizados ou com nomenclatura inconsistente são um dos erros mais comuns ao criar REST API. Imagine um dev que está desenvolvendo uma API e define rotas como
/getAllUsers
para buscar usuários ou /delete-user
para deletar um usuário. Além disso, ele mistura singular e plural sem critério (hora /user
, hora /users
) e adiciona verbos na URL. Esses padrões confusos geram APIs difíceis de entender e manter. Quem consome a API pode ficar perdido sem saber se deve usar /clientes
ou /listarClientes
para obter a lista de clientes. Em resumo, endpoints mal planejados resultam em design de API pobre, confuso e propenso a erros.Solução: defina uma estrutura clara e padronizada para seus endpoints REST. As boas práticas incluem: usar substantivos (nomes de recursos) em vez de verbos, usar plural para recursos colecionáveis e organizar hierarquicamente conforme a relação entre recursos. Mantenha os nomes em letras minúsculas e, se precisar separar palavras, use hífen (
-
) ou camelCase consistentemente (hífen é comum em URLs). Evite caracteres especiais ou espaços. Seguindo essas regras, a API fica intuitiva tanto para devs Rocketseat quanto para qualquer consumidor.Por exemplo, para um recurso de usuários:
// Exemplo ruim (endpoint com verbo e nomenclatura não padronizada): app.get('/getAllUsers', ...); // Bom exemplo (endpoint descritivo, substantivo plural): app.get('/users', (req, res) => { const users = [ { id: 1, nome: 'Diego' }, { id: 2, nome: 'Mayk' } ]; res.json(users); });
No exemplo acima, usamos
GET /users
para obter a lista de usuários. Notou a diferença? Endpoints REST bem nomeados são autoexplicativos: qualquer pessoa sabe que GET /users
retorna usuários sem precisar de documentação extra. Da mesma forma, use POST /users
para criar um novo usuário, GET /users/1
para obter o usuário com ID 1, PUT /users/1
ou PATCH /users/1
para atualizar, e DELETE /users/1
para remover. Caso haja recursos relacionados, utilize caminhos aninhados de forma lógica – por exemplo, os posts de um usuário poderiam ser acessados em /users/1/posts
.Outra dica: utilize parâmetros de rota e query adequadamente. Parâmetros de rota (por exemplo,
:id
) para identificar recursos únicos, e query params para filtros, paginação ou outras opções não hierárquicas. Ex: GET /users/1/orders?status=completed
(rota aninhada para pedidos de um usuário, com query param para filtrar por status). Isso evita endpoints customizados como /getCompletedOrdersByUserId
, que fogem do padrão REST.Resumindo, padronize a nomenclatura de endpoints REST:
- Use nomes de recursos (substantivos) em vez de ações.
- Prefira plural para coleções (ex:
/users
para coleção de usuários).
- Mantenha URLs legíveis e consistentes (minúsculas, separadores padronizados).
- Estruture URLs refletindo relações de dados (endpoints aninhados quando necessário, mas sem exagero de profundidade).
Com essas práticas, você terá endpoints limpos, organizados e fáceis de consumir, e evitará a confusão que a falta de padrão causa.
Uso correto de métodos HTTP
Problema: outro erro comum em REST API é usar métodos HTTP de forma incorreta, fugindo do que foi definido pelos padrões REST. Já vi casos em que um determinado desenvolvedor(a) implementou quase tudo com
POST
em sua API porque "é o que o formulário HTML mandava", até mesmo para buscar dados! Em outro cenário, fiz um review de uma estagiária que criou uma rota GET /users/delete/1
para remover um usuário – abusando do verbo no endpoint e ignorando o método apropriado. Essas abordagens funcionam? Podem até funcionar, mas violam o sentido semântico dos métodos HTTP e tornam o sistema menos intuitivo e até menos seguro (um web crawler poderia acidentalmente disparar um GET destrutivo, por exemplo). Clientes da API ficam confusos se devem usar GET ou POST em certas operações, já que o design está inconsistente.Solução: em uma REST API bem projetada, cada método HTTP possui uma finalidade específica, e devemos respeitá-las ao criar os endpoints.
Clique aqui e veja como as principais operações CRUD se relacionam com os métodos:
- GET – Leitura (Read): obter dados do servidor. Deve ser seguro e sem efeitos colaterais, ou seja, chamar o mesmo GET várias vezes não muda o estado do servidor (é uma operação idempotente e de consulta). Exemplo:
GET /users
retorna a lista de usuários.
- POST – Criação (Create): enviar dados para criar um novo recurso. Não é idempotente (chamadas repetidas podem criar duplicatas). Exemplo:
POST /users
com um corpo JSON criando um novo usuário (por exemplo,{ "nome": "Laís", "email": "lais@example.com" }
).
- PUT – Substituição (Update completa): atualizar por completo um recurso existente, enviando todo o estado novo. É idempotente (várias chamadas com o mesmo corpo resultam no mesmo estado). Exemplo:
PUT /users/1
atualiza completamente o usuário de ID 1 (todos os campos).
- PATCH – Atualização parcial (Partial Update): alterar parte de um recurso existente. Usa um payload apenas com os campos a modificar. Também deve ser idempotente se aplicado o mesmo patch repetidas vezes (assumindo mesma operação). Exemplo:
PATCH /users/1
com{ "nome": "Laís Lemes" }
altera apenas o nome do usuário 1.
- DELETE – Remoção (Delete): excluir um recurso. Idealmente idempotente (deletar já deletado continua resultando em recurso inexistente). Exemplo:
DELETE /users/1
remove o usuário 1.
Seguir essas convenções torna o design da API intuitivo. Ferramentas, desenvolvedores(as) e documentação já esperam esse comportamento padrão. Se você vê um endpoint
DELETE /users/1
, imediatamente entende que remove o usuário 1, sem precisar de explicação extra.No Node.js com Express/Fastify, implementar isso é simples e expressivo. Por exemplo, usando Express + TypeScript:
import express from 'express'; const app = express(); app.use(express.json()); // GET: obter lista de usuários app.get('/users', (req, res) => { // ... código para pegar usuários res.status(200).json([...]); }); // POST: criar novo usuário app.post('/users', (req, res) => { const novoUsuario = req.body; // ... código para salvar novoUsuario res.status(201).json({ message: 'Usuário criado com sucesso!', user: novoUsuario }); }); // DELETE: remover usuário por ID app.delete('/users/:id', (req, res) => { const userId = req.params.id; // ... código para deletar usuário de id = userId res.status(204).send(); // 204 No Content (remoção sem retornar conteúdo) });
No código acima, usamos os verbos do Express correspondentes aos métodos HTTP adequados. Note o uso de
res.status(201)
para criação e res.status(204)
para deleção sem conteúdo (falaremos de códigos de status no próximo tópico). O importante aqui é: não abuse de métodos fora de contexto. Se precisa disparar uma ação que não se encaixa bem em CRUD (por exemplo, um endpoint para login de usuário que verifica credenciais), você pode usar POST /login
(faz sentido, pois está criando uma sessão/token). Se for uma ação específica em um recurso, algumas opções são: ou modelar como um recurso (ex: em vez de /users/1/activate
poderia ser POST /users/1/activations
criando um recurso de "ativação"), ou, se fugir muito do CRUD, ainda assim utilize POST (ações customizadas geralmente usam POST por ser uma operação). Evite usar GET para ações que modificam dados ou POST para pegar dados, pois isso quebra a semântica REST.Lembre-se: ao consumir APIs REST, você também espera que elas sigam essas convenções. Portanto, forneça essa consistência para os consumidores da sua API. Cada método no lugar certo deixa a integração mais fácil e reduz erros.
Códigos de Status HTTP
Problema: utilizar HTTP status codes incorretamente (ou ignorá-los) é um erro que prejudica tanto o front-end quanto o debug do back-end. Um cenário comum: programador(a) desenvolveu um endpoint de cadastro e, mesmo quando ocorriam erros de validação (como email já cadastrado), a API retornava
200 OK
com uma mensagem de erro no corpo. Em outro caso, um dev front-end consumindo a API, não conseguia distinguir pelo código se a chamada tinha falhado, pois tudo vinha com status 200. Também há quem retorne sempre 500 para qualquer problema, mesmo quando é um erro do cliente (request incorreta). Esses erros comuns de REST API no tratamento de status confundem quem consome a API e fogem dos padrões REST de comunicação.Solução: utilize apropriadamente os códigos de status HTTP para cada situação, indicando claramente o resultado da requisição. Os códigos de status são a forma do servidor comunicar ao cliente de forma padronizada o que aconteceu.
Clique aqui e veja algumas práticas recomendadas.
- 200 OK: requisição bem-sucedida em geral. Use para retornos padrão de GET, PUT, PATCH ou qualquer operação de sucesso que não crie um novo recurso. Exemplo:
GET /users/1
retorna 200 se o usuário existe (e inclua no corpo os dados do usuário).
- 201 Created: recurso criado com sucesso. Utilize em respostas de criação via POST (ou PUT que criou algo). Idealmente, inclua no header
Location
a URL do novo recurso criado, e/ou retorne o objeto criado no corpo. Exemplo:POST /users
retorna 201 e talvez{ id: 3, nome: 'Isabela', ... }
.
- 204 No Content: sucesso sem conteúdo no corpo. Muito útil para operações de DELETE (ao deletar não há mais conteúdo a retornar) ou respostas a PUT/PATCH quando não há necessidade de retornar o recurso atualizado. Exemplo:
DELETE /users/1
-> 204 No Content.
- 400 Bad Request: a requisição está malformada ou contém dados inválidos. Use quando faltam campos obrigatórios, formatos estão errados ou a lógica de negócio detecta um erro no input do cliente. Ex: enviar um JSON inválido ou um valor fora do esperado.
- 401 Unauthorized: falha de autenticação – use quando a requisição requer autenticação (login/token) e o cliente não forneceu credenciais válidas. Exemplo: token de acesso faltando ou inválido ao acessar um endpoint protegido.
- 403 Forbidden: autenticação feita, porém cliente não tem permissão para aquele recurso. Ex: token válido, mas usuário tentando acessar um recurso admin do Diego. Retorne 403 para dizer "entendi quem você é, mas você não pode acessar isso".
- 404 Not Found: recurso não encontrado. Se o cliente pede, por exemplo,
GET /users/999
e o usuário 999 não existe, retorne 404. Assim o consumidor sabe que aquele recurso não existe ou foi removido.
- 409 Conflict: conflito de estado atual do recurso com a requisição. Ex:
POST /users
tentando criar um usuário com email que já existe – alguns optam por 409 em vez de 400 para indicar o conflito específico (ou 422 Unprocessable Entity).
- 422 Unprocessable Entity: muito usado para erros de validação de entidade. Por exemplo, validação de formulário passou formato JSON mas os dados não satisfazem os critérios (email inválido, etc.). Similar ao 400, mas semanticamente indica "entendi o request, mas dados são inválidos".
- 500 Internal Server Error: erro inesperado no servidor. Use quando algo falhou do lado do servidor (exceção não tratada, falha de conexão a BD, etc.). Não use 500 para erros previsíveis causados pelo cliente (como validação) – reserve 500 para exceções/bugs.
No Node.js (Express), você pode definir o status facilmente antes de enviar a resposta, usando
res.status(code)
ou atalhos como res.sendStatus(code)
. Por exemplo:app.get('/users/:id', (req, res) => { const { id } = req.params; const user = users.find(u => u.id === Number(id)); if (!user) { return res.status(404).json({ erro: 'Usuário não encontrado' }); } res.status(200).json(user); });
No snippet acima, se o usuário não for encontrado, retornamos 404 Not Found com uma mensagem de erro no corpo; caso contrário, 200 com o usuário. Essa comunicação clara via código de status permite que qualquer cliente (um frontend React, por exemplo, ou um script do Insomnia/Postman) trate adequadamente o caso de erro, exibindo "Usuário não encontrado" quando for 404, ou prosseguindo quando recebe 200.
Dica: mantenha consistência também nas mensagens de erro no corpo (veremos no próximo tópico). Mas nunca ignore o código! Muitos desenvolvedores(as) front-end, olham primeiro para o status da resposta antes de ler o corpo. Um 400 ou 500 acende um alerta imediato que algo deu errado, enquanto um 200 com erro no corpo pode passar despercebido ou confundir.
Em resumo, use os HTTP status codes a seu favor para comunicar sucesso ou falha. Essa é uma das boas práticas REST API fundamentais para um design de API profissional.
Padrão nos payloads (request/response)
Problema: inconsistência no formato de dados enviados ou recebidos (payloads) é um erro sutil, porém extremamente frustrante. Imagine consumir uma API que às vezes retorna
{ "success": true, "data": {...} }
e em outros endpoints retorna diretamente o objeto sem esse wrapper. Ou, pior, cada endpoint retorna campos com convenções diferentes (em um, as chaves são em camelCase, noutro em snake_case). Já vi casos em que a API, desenvolvida, para certos erros respondia com texto puro ("Internal Server Error") ao invés de JSON, enquanto para outros erros retornava um JSON com detalhes. Essas inconsistências de payload tornam a integração caótica: você precisa tratar vários formatos diferentes, o que aumenta a chance de bugs e confusão. Fere também a ideia de padrões REST e de contrato claro entre client-server.Solução: padronize os formatos de request e response da sua API. Isso envolve adotar convenções consistentes de nomenclatura de campos, estrutura de envelopes de resposta e formato de dados.
Veja aqui algumas boas práticas para manter seus payloads coerentes:
- JSON como padrão: em aplicações web modernas, JSON é o formato dominante para REST APIs. Mantenha todas as suas respostas em JSON (a não ser que haja um requisito específico para XML ou outro formato, mas foquemos em JSON). Configure o
Content-Type: application/json
adequadamente e documente que sua API aceita/retorna JSON.
- Convenção de nomes consistente: escolha entre camelCase ou snake_case para as chaves dos objetos JSON e seja consistente em toda a API. Exemplo: se você decide usar camelCase (comum em JavaScript/TypeScript), então todas as propriedades devem seguir isso (ex:
firstName
,createdAt
). Não misture comfirst_name
oucreated_at
em outros endpoints.
- Estrutura uniforme de respostas de sucesso: decida se vai envelopar a resposta ou não. Muitos optam por retornar diretamente o recurso ou dados solicitados sem um wrapper. Ex:
GET /users/1
retorna{ "id": 1, "nome": "Diego", "idade": 25 }
. Outros preferem um envelope padrão, especialmente se querem incluir metadados: ex:{ "data": { ...recurso... }, "meta": { ... } }
. Ambos funcionam; o importante é ser consistente. Para fins didáticos, vamos supor respostas diretas para dados solicitados, e envelopes apenas quando necessário (paginação, listas com meta, etc.).
- Estrutura uniforme de respostas de erro: este item é crucial. Defina um padrão para erros. Pode ser um campo
error
booleano +message
, ou um objeto de erro com mais detalhes. Exemplo simples e eficaz: retornar sempre{ "erro": "Mensagem explicando o problema" }
em caso de erro (podendo ter campos adicionais comoerrors
com uma lista de detalhes, oucode
interno, etc.). O importante é que o cliente não tenha que lidar às vezes com texto puro, às vezes JSON, ou chaves diferentes para mensagens. Padronize algo como{"error": "...", "message": "Detalhes"}
ou apenas{"message": "Detalhes do erro"}
.
- Exemplo de erro consistente: se o cliente manda um payload inválido, você pode retornar
400 Bad Request
com:
{ "erro": "Requisição inválida", "mensagem": "O campo email é obrigatório." }
Se outro endpoint tiver um erro similar, siga o mesmo formato de resposta de erro, mudando apenas a mensagem e detalhes. Não invente formatos novos a cada situação.
- Versionamento e retrocompatibilidade de payload: quando você evoluir sua API (veremos adiante sobre versionamento de API), tome cuidado para não quebrar o formato esperado pelos clientes antigos. Se precisar mudar significativamente o formato da resposta, provavelmente é hora de versionar a API.
Em Node.js com TypeScript, você pode aproveitar interfaces ou types para definir a forma dos objetos que trafegam na API, garantindo no build que está seguindo o contrato. Por exemplo:
// Definição de interface para User interface User { id: number; nome: string; email: string; idade?: number; } // Exemplo de função Express que retorna um usuário app.get('/users/:id', (req, res) => { const user: User | undefined = /* lógica para obter usuário */; if (!user) { return res.status(404).json({ erro: 'Usuário não encontrado' }); } res.status(200).json(user); });
No snippet acima, definimos a interface
User
para tipar nossos objetos de usuário. Assim, garantimos que sempre retornaremos as mesmas propriedades (id
, nome
, email
, etc.) naquele endpoint, reduzindo chances de inconsistência. Além disso, para erros estamos sempre usando a chave erro
com uma mensagem consistente.Stateless API: outro ponto de padrão REST é que a API deve ser stateless (sem estado de sessão no servidor). Isso significa que cada requisição deve conter todas as informações necessárias (por exemplo, token de autenticação, ID de recurso, etc.), sem depender de contexto armazenado em memória do servidor entre chamadas. Ser stateless não é exatamente formatação de payload, mas está relacionado à consistência do protocolo. Por exemplo, se sua API requer autenticação, não guarde sessões em memória; prefira um token JWT enviado em cada requisição (aprofundaremos sobre isso no tópico sobre segurança) – assim cada request é independente. Isso facilita escalar e manter a coerência do comportamento (qualquer instância de servidor pode responder sem precisar de session stickiness). Padrões REST pedem tanto formatação consistente quanto ausência de estado compartilhado no servidor.
Mantendo os payloads padronizados e a API stateless, você proporciona uma experiência muito melhor para quem consome a API (eles podem reutilizar código de parsing de resposta, tratar erros de forma genérica, etc.) e torna a manutenção do servidor mais tranquila.
Versionamento de APIs
Problema: evolução sem controle pode virar dor de cabeça. Imagine que você lançou a versão inicial da API (v1) de uma aplicação. Com o tempo, novos requisitos surgiram e você acabou alterando campos nas respostas e adicionou novas regras nos endpoints existentes sem versionar. De repente, o app mobile que consumia a API quebrou, porque esperava um campo
nome
que foi renomeado para nomeCompleto
, e algumas requisições passaram a falhar. Esse erro comum – falta de versionamento – leva a quebras de compatibilidade e clientes frustrados. Por outro lado, alguns até tentam versionar, mas fazem de forma inconsistente (ex: alguns endpoints com /v1
no caminho e outros não, ou versionam só quando dá problema, ao invés de planejar antes).Solução: adote uma estratégia de versionamento de API desde cedo, garantindo que mudanças significativas no contrato não afetem clientes antigos inesperadamente. A forma mais simples e comum é incluir a versão na URL dos endpoints, por exemplo prefixando com
/v1
, /v2
, etc. Assim, ao precisar fazer modificações incompatíveis (breaking changes), você cria uma nova versão v2, mantendo a v1 ativa para quem ainda precisa dela.Confira aqui boas práticas para versionamento:
- Planeje o versionamento: desde o início, já pense sua API com um
v1
implícito. Mesmo que você não coloque/v1
em todos os caminhos enquanto está em desenvolvimento inicial, esteja pronto para lançar uma versão 1 formal quando estabilizar o contrato.
- Versão na URL: exemplo, defina suas rotas sob um prefixo
/api/v1/
. Em Express, você pode fazer:
const v1 = express.Router(); v1.get('/users', getUsersHandler); v1.post('/users', createUserHandler); // ... demais rotas v1 app.use('/api/v1', v1);
Assim, todas as rotas v1 começam com
/api/v1
. Quando surgir a necessidade de v2, você poderá criar um novo router para v2 com mudanças.- Versionamento por header (alternativo avançado): algumas APIs versionam via header (por exemplo,
Accept: application/vnd.minhaapi.v2+json
). Isso mantém a URL “limpa”, mas adiciona complexidade de negociação de conteúdo. Para fins de didática e praticidade, o versionamento via URL é suficiente e claro.
- Documentação de versões: deixe claro aos consumidores qual versão estão usando e o que mudou de uma versão para outra. Por exemplo, changelog ou documentação separada por versão.
- Deprecação gradual: se precisar desligar uma versão antiga, comunique os usuários com antecedência. Algumas empresas retornam um header ou campo nas respostas da versão antiga informando que ela será descontinuada. Sempre dê tempo para migração.
- Backward compatibility: pequenas mudanças que não quebram compatibilidade (como adicionar um novo campo que clientes antigos vão ignorar) não precisam de versão nova. Mas remover ou alterar significado de campos existentes, ou mudar URIs, requer versão nova.
Em um cenário prático: suponha que na versão 1 da API
/api/v1/users
retorna uma lista de usuários com campos id, nome, email
. No futuro, você decide que quer mudar nome
para nomeCompleto
e também retornar o campo idade
. Se você fizer isso em /v1
diretamente, quem consumia nome
vai quebrar. A abordagem correta é criar /api/v2/users
que retorna id, nomeCompleto, email, idade
. A versão 1 continua disponível talvez depreciada, mas funcional, retornando do jeito antigo para não quebrar aplicações legadas.Com Node.js/Express, manter dois conjuntos de rotas (v1 e v2) é apenas uma questão de organização de código. Você pode separar em módulos diferentes. Em TypeScript, aproveite para reutilizar lógicas internas quando possível, mas as camadas de roteamento/controladores podem se diferenciar conforme a versão. Exemplo simplificado:
// V1 app.get('/api/v1/users', (req, res) => { res.json([ { id: 1, nome: 'Diego', email: 'diego@example.com' } ]); }); // V2 app.get('/api/v2/users', (req, res) => { res.json([ { id: 1, nomeCompleto: 'Diego Fernandes', email: 'diego@example.com', idade: 25 } ]); });
Perceba que o v2 mudou a estrutura. Um cliente v1 continuaria recebendo
nome
e não saberia de nomeCompleto
, mantendo compatibilidade.Versionar a REST API é crucial para manter uma evolução saudável. Isso demonstra maturidade no projeto e respeito pelos consumidores. Grandes empresas (Google, Meta etc.) versionam suas APIs; Alunos da Rocketseat não fazem diferente: fornecem uma API que evolui sem deixar ninguém para trás!
Segurança em APIs REST (HTTPS, JWT e validações)
Problema: segurança muitas vezes é deixada de lado no começo de projetos, o que é um erro grave. Já vimos situações em que a API estava rodando em HTTP sem criptografia, expondo dados sensíveis. Por exemplo, um dev lançou um MVP rápido e deixou tokens de usuário trafegarem via HTTP – um interceptor malicioso na rede poderia roubar essas credenciais facilmente. Em outro caso, uma desenvolvedora implementou autenticação com JWT, mas não validava corretamente o token nem verificava autorização, permitindo acesso indevido a dados. Além disso, negligenciar validações de entrada pode abrir brechas: como a possibilidade de cadastrar usuários sem email ou até injetar scripts nos campos. Esses erros comuns de segurança em API REST comprometem tanto os dados dos usuários quanto a estabilidade do sistema (imagina uma falta de validação permitindo um SQL Injection ou um payload gigante derrubando o servidor).
Solução: trate segurança como cidadã de primeira classe no design da sua API. Vamos dividir as boas práticas de segurança em três pilares principais: uso de HTTPS, autenticação/autorização (com JWT como exemplo) e validação de dados.
1. Use HTTPS sempre que possível: em produção, nunca exponha sua API REST sem criptografia. HTTPS garante que os dados trafegados entre cliente e servidor estão criptografados, impedindo espionagem e ataques man-in-the-middle. Plataformas em nuvem e hospedagens modernas facilitam isso (muitas já geram certificados SSL gratuitamente, ou usam proxies). Certifique-se de que seu cliente (frontend, mobile, etc.) faz requisições
https://
ao invés de http://
. Em ambientes de desenvolvimento, você pode usar http local, mas tenha atenção redobrada para não deixar em produção sem TLS. Com Express, não precisa configurar nada especial na aplicação (geralmente a infra cuida do TLS), mas se você usar Node puro, procure usar bibliotecas ou o módulo https
com certificados válidos. Resumindo: segurança API REST começa com HTTPS.2. Autenticação e autorização (JWT): grande parte das APIs precisa restringir acesso a certos endpoints. O padrão moderno é usar JWT (JSON Web Token) para autenticação stateless. Funciona assim: o usuário faz login enviando credenciais (por exemplo,
POST /login
com email/senha), a API verifica e responde com um token JWT assinado (gerado com uma chave secreta que só o servidor conhece). Esse token é então enviado pelo cliente em cada requisição subsequente, geralmente no header Authorization: Bearer <token>
.Quer ver um exemplo? Clique aqui.
Suponha que você faça login e receba um token. Ao acessar
GET /api/v1/mentoria
(um recurso protegido), envia o token no header. No servidor Express, temos um middleware para verificar o JWT:import jwt from 'jsonwebtoken'; const authMiddleware = (req, res, next) => { const authHeader = req.headers['authorization']; if (!authHeader) { return res.status(401).json({ erro: 'Token não fornecido' }); } const token = authHeader.split(' ')[1]; try { const payload = jwt.verify(token, process.env.JWT_SECRET); req.user = payload; // payload contém info do usuário (ex: id, role) next(); // usuário autenticado, prossegue } catch (e) { return res.status(403).json({ erro: 'Token inválido ou expirado' }); } }; // Uso do middleware em uma rota protegida: app.get('/api/v1/mentoria', authMiddleware, (req, res) => { // Se chegou aqui, req.user está definido res.json({ message: `Bem-vindo à área de mentoria, ${req.user.name}!` }); });
No código acima, cobrimos dois cenários com status code adequado: se não há token, retornamos 401 Unauthorized (cliente não autenticado); se o token é inválido ou expirou, retornamos 403 Forbidden (autenticação presente, porém rejeitada). Isso segue as boas práticas HTTP. Perceba também que usamos
process.env.JWT_SECRET
– ou seja, a chave secreta está em variável de ambiente, não hard-coded (nunca exponha segredos no código!).Além da autenticação, cuide da autorização: nem todo usuário autenticado pode acessar tudo. Você pode incluir no JWT informações de cargo/permits (ex:
role: 'admin'
) e, nos endpoints, checar se req.user.role
tem permissão. Se não, retorne 403. Ex: somente admin pode acessar certos endpoints, enquanto usuário comum não.Dica: defina um tempo de expiração razoável para tokens JWT (por exemplo, 15 min ou 1h, mais refresh tokens se precisar). Assim você mitiga risco de tokens vazados serem usados indefinidamente. Também prefira JWTs com assinatura HMAC (HS256) ou RS256, e bibliotecas consagradas para validação (como jsonwebtoken mesmo). Nunca tente "fazer seu próprio token" ou armazenar senhas em texto plano – use bcrypt para hash de senhas no banco e tokens para sessão.
3. Validação de dados de entrada: nunca confie cegamente nos dados que chegam do cliente. Mesmo que você tenha validação no front-end, um usuário malicioso pode enviar requisições diretamente à sua API com dados inconsistentes ou maliciosos. Portanto, valide tudo no backend. Isso inclui parâmetros de rota, query strings, e especialmente o corpo (JSON) de requisições POST/PUT/PATCH.
Algumas das checagens obrigatórias são:
- Campos obrigatórios: verifique que todos os campos necessários estão presentes. Ex: no cadastro de usuário, se
email
ounome
são obrigatórios, rejeite (400 Bad Request
) caso faltem.
- Tipos e formatos: cheque se os campos têm o tipo/formato esperado. Ex:
email
parece um email válido?idade
é um número? Use regex ou bibliotecas para validar formatos (por exemplo, validator.js para emails).
- Restrições de negócio: impônha limites necessários (ex: senha deve ter X caracteres, idade >= 0, datas válidas, etc.).
- SQL/NoSQL Injection: se você está passando entradas para um banco de dados, proteja-se contra injeção. Use queries parametrizadas ou ORM que já faça isso. Ex: nunca monte query SQL concatenando strings direto do input.
- XSS e Injection em geral: se algum dado vai ser armazenado e exibido em apps web, sanitize entradas para evitar código malicioso. Por exemplo, remover scripts de campos de texto (ou usar bibliotecas de sanitização).
- Tamanho dos payloads: uma forma de ataque DoS é enviar um payload gigante. Você pode mitigar definindo limites no body parser. O Express por exemplo, com
express.json({ limit: '1mb' })
define limite de 1 MB no JSON de entrada. Assim você evita que alguém mande 1GB de JSON e trave seu servidor.
Você pode implementar validação manualmente ou usar bibliotecas de validação para ajudar. Em TypeScript, libraries como Joi, Yup ou Zod permitem definir esquemas e validar objetos facilmente. Por exemplo, com Zod:
import { z } from 'zod'; const userSchema = z.object({ nome: z.string().min(1), email: z.string().email(), idade: z.number().int().nonnegative().optional() }); app.post('/api/v1/users', (req, res) => { const parseResult = userSchema.safeParse(req.body); if (!parseResult.success) { return res.status(400).json({ erro: 'Dados inválidos', detalhes: parseResult.error.errors }); } // se válido, prossegue para criar usuário const userData = parseResult.data; // ... salvar userData res.status(201).json({ message: 'Usuário criado', user: userData }); });
Podemos concluir que, segurança de API REST envolve várias frentes. As principais dicas resumidas:
- Sempre use HTTPS em produção.
- Autentique as requisições com tokens (ex: JWT) e verifique permissões quando aplicável.
- Valide e sanitize todos os dados vindos do cliente.
- Mantenha sua API stateless no que tange à autenticação (não use sessão de servidor, use token).
- Utilize ferramentas auxiliares: bibliotecas de segurança (Helmet no Express para cabecalhos seguros, rate limit para evitar abuso de requests, CORS configurado corretamente para evitar acessos indevidos de origens não autorizadas).
Seguindo essas práticas, você protege os dados dos usuários e constrói uma API confiável. Lembre-se: uma API insegura pode comprometer todo o sistema, então segurança não é opcional.
Performance e escalabilidade (paginação e otimização de consultas)
Problema: sua API funciona, é segura e bem estruturada... mas e quando os dados crescem ou a quantidade de usuários dispara? Sem pensar em performance e escalabilidade, você pode ter surpresas desagradáveis.
Aqui estão alguns dos erros mais comuns, quando o assunto é performance e escalabilidade:
- Falta de paginação: criar um endpoint
GET /posts
que retorna todos os posts de uma plataforma. No início funciona bem (poucos posts), mas conforme o número de posts cresce, esse endpoint fica lento e começa a retornar respostas enormes, consumindo muita banda e até estourando a memória do cliente. Sem paginação ou limites, a performance despenca.
- Consultas não otimizadas (N+1 queries): Imagine o seguinte cenário, uma programadora implementou
GET /users
que, para cada usuário, buscava também seus pedidos em outra tabela, mas fez isso dentro de um loop (para cada usuário fazia uma query separada). O resultado? 100 usuários geravam 101 queries no banco (1 pra lista de usuários + 100 pra pedidos), sobrecarregando o banco e aumentando a latência.
- Não uso de índices ou filtros inadequados: Certa vez, eu me deparei com a seguinte situação: a busca de produtos
GET /products?name=abc
estava lenta porque a consulta no banco não utilizava índice (talvez esqueceu de indexar a coluna nome), fazendo full scan em milhares de registros a cada requisição.
- Stateful server impedindo escalabilidade horizontal: um erro comum é colocar um cache em memória no servidor para armazenar sessões de usuário. Quando escalar a aplicação para múltiplas instâncias, os usuários provavelmente ficarão presos a um servidor (ou perderão a sessão ao cair em outro) – isso porque a API não é stateless na prática, dificultando o balanceamento de carga.
- Ausência de cache ou uso ineficiente de recursos: com nenhum mecanismo de cache presente, até mesmo consultas idênticas repetidas tem que refazer todo o trabalho do zero ou talvez nenhuma compressão de resposta ativada, enviando payloads enormes desnecessariamente.
Solução: para atingir alta performance e escalabilidade em sua REST API, aplique algumas estratégias fundamentais:
1. Paginação e limitação de resultados: para endpoints que retornam listas potencialmente grandes, implemente paginação ou algum tipo de limite. Isso reduz drasticamente a carga em cada requisição e melhora a experiência do cliente que não precisa esperar todos os dados carregarem quando talvez só os primeiros 20 itens importam inicialmente.
- Paginação tradicional: use parâmetros como
?page=1&limit=50
. A requisiçãoGET /posts?page=2&limit=50
retornaria os posts de 51 a 100, por exemplo. No response, você pode incluir cabeçalhos ou campos indicando quantos resultados totais existem, qual a próxima página, etc. Exemplo de resposta com meta:
{ "data": [ /* array de até 50 posts */ ], "page": 2, "per_page": 50, "total": 247, "total_pages": 5 }
Assim, o cliente pode paginar conforme necessário.
- Limite padrão: mesmo que não implemente toda uma paginação com página e etc., tenha pelo menos um limit padrão. Por exemplo,
GET /posts
retorna por padrão 100 itens no máximo, e pode aceitar um query param?limit=200
até um teto estabelecido. Nunca retorne 10 mil registros de uma vez sem necessidade.
- Scroll ou cursores: em casos de alta escala, você pode usar paginação baseada em cursor (ex:
?after=id_ultimo_recebido
), muito comum em APIs de redes sociais, para eficiência. Mas para desenvolvedores(as) iniciantes, entender a paginação simples já é um ótimo começo.
Implementar paginação em Node depende de como você busca os dados:
- Se for um banco de dados SQL, use
LIMIT
eOFFSET
nas queries.
- Em MongoDB, use
.limit(n).skip(n*page)
ou preferencialmente cursos com_id
> last_id.
- ORMs geralmente têm métodos de paginação (ex:
.findMany({ skip: 50, take: 50 })
no Prisma, ou.limit().offset()
no Sequelize/TypeORM).
Exemplo de pseudo-código:
app.get('/api/v1/posts', async (req, res) => { const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 50; const offset = (page - 1) * limit; const [results, total] = await db.Post.findAndCountAll({ limit, offset }); // exemplo ORM res.json({ data: results, page, per_page: limit, total, total_pages: Math.ceil(total/limit) }); });
Assim você envia só uma página por vez.
2. Otimização de consultas e acesso a dados: esteja atento ao desempenho do seu banco de dados e camada de dados:
- Use índices adequadamente: identifique quais campos são mais filtrados (por exemplo, email de usuário, nome de produto) e certifique-se de indexá-los no banco de dados. Índices aceleram muito buscas. Em ORMs ou query builders, às vezes você precisa definir isso no esquema. Em SQL puro, crie índices via migrations.
- Evite N+1 queries: Se você precisa carregar dados relacionados, busque maneiras de fazer isso em uma consulta otimizada. ORMs normalmente oferecem algo como eager loading (ex:
.include
no Prisma,.populate
no Mongoose,JOIN
manual em SQL). Exemplo: ao buscar usuários com seus pedidos, faça uma consulta que já traga os pedidos de cada usuário, em vez de uma pra cada.
- Cache de consultas frequentes: se certos endpoints fazem consultas pesadas mas com resultados que repetem (ex: dados que mudam pouco, ou listas públicas), considere implementar um cache. Pode ser na memória (Node cache, Redis) ou nível de banco (materialized views, etc.). Em Node, integrar com Redis para cache de algumas respostas pode aliviar o banco de dados.
- CDN e cache HTTP: para dados públicos que não mudam a cada segundo, use cabeçalhos HTTP de cache (
Cache-Control
) para permitir que clientes e intermediários (CDNs) cacheiem as respostas. Ex: uma lista de produtos que muda diariamente poderia terCache-Control: public, max-age=300
(5 minutos). Isso reduz chamadas repetidas.
- Compressão de resposta: ative gzip/deflate. O Express tem o middleware
compression()
que comprime as respostas, economizando banda e tempo de transferência, especialmente para payloads JSON grandes.
- Evite processamento desnecessário no servidor: por exemplo, não gere dados que o cliente não usa. Se a API tem opção de filtro de campos (fields), pode implementar para retornar só o necessário. Mas principalmente, não faça loops ou transformações custosas se puder delegar ao banco de dados ou a bibliotecas eficientes.
3. Escalabilidade horizontal (statelessness & clustering): Já falamos sobre manter a API stateless (sem sessão server-side) – isso permite escalar facilmente adicionando mais instâncias de servidor, pois nenhuma depende de contexto local. Use um load balancer para distribuir requisições. No Node, você pode usar o módulo
cluster
ou PM2 para usar múltiplos processos do app (aproveitando multi-core) ou conteinerização/orquestração (Docker, Kubernetes) para rodar várias instâncias. O importante: com ausência de estado, qualquer instância pode atender qualquer request. Se precisar compartilhar algo (ex: contagem de online), use um repositório central (DB, cache).- Mantenha também um olho em monitoramento e logging. Uma API escalável precisa ser observável: use logs (lib como morgan para logs HTTP, ou Winston para logs gerais) e monitore métricas (tempo de resposta, uso de CPU/RAM). Assim você antecipa gargalos.
- Rate limiting: Para escalabilidade e segurança, implemente limites de requisição por IP/usuário para evitar abuso (ataques de DDoS ou apenas uso excessivo). Libs como
express-rate-limit
podem ajudar, ou no próprio proxy (NGINX etc.).
Resumindo as dicas de performance:
- Implemente paginação ou limites em endpoints de listagem.
- Otimize consultas de banco (índices, evitar N+1, busca eficiente).
- Use cache onde fizer sentido e comprimir respostas.
- Mantenha a API stateless para escalar horizontalmente facilmente.
- Considere balanceamento de carga e uso eficiente de recursos (multi-thread/process, etc.).
Com essas práticas, sua API estará pronta para crescer. Seja 100 ou 1 milhão de usuários, uma base bem pensada permitirá que a experiência continue rápida e fluida. Lembre-se: a experiência do usuário final muitas vezes depende da performance da API – ninguém gosta de esperar ou de ver um app travar por um excesso de dados. Então, assim como ensinamos na Rocketseat, pense em desempenho desde o início!
Conclusão
Ufa! Percorremos muitos aspectos fundamentais de REST API – desde a concepção dos endpoints até detalhes de segurança e performance – sempre com um olhar nas boas práticas REST API e em como evitar os erros comuns que tantos desenvolvedores(as) já enfrentaram. Com exemplos práticos em Node.js e TypeScript, vimos na prática como aplicar cada conceito. Agora você está mais preparado(a) para criar REST APIs em Node.js seguindo padrões profissionais: endpoints bem organizados, métodos HTTP corretos, códigos de status significativos, payloads consistentes, versionamento inteligente, segurança robusta e preocupação com desempenho e escalabilidade.
Como um(a) desenvolvedor(a) backend em evolução, absorver esses conceitos é dar um passo importante para alcançar o próximo nível. Lembre-se que design de APIs é tanto uma arte quanto uma ciência: pense na experiência de quem consome, mantenha as convenções REST em mente (como a ideia de stateless API e recursos bem definidos) e não pare de aprender e refinar seu trabalho.
E por falar em dar o próximo passo na sua jornada dev... que tal contar com a ajuda de quem vive e respira essas boas práticas no dia a dia? Na Formação Node.js da Rocketseat, você vai aprender na prática como construir APIs REST eficientes, seguras e escaláveis do zero. Prepare-se para mergulhar fundo em Node.js, Express, Fastify, bancos de dados, autenticação JWT, testes, deploy e muito mais – tudo com o acompanhamento dos experts e uma comunidade vibrante para te apoiar.
Artigos_
Explore conteúdos relacionados
Descubra mais artigos que complementam seu aprendizado e expandem seu conhecimento.