Toda API começa pequena. Dois ou três endpoints, um cliente consumindo, tudo rápido de entregar. O problema começa quando chega o quarto desenvolvedor, o segundo aplicativo consumidor, e de repente você percebe que a estrutura que fez sentido quando você estava sozinho não faz mais sentido nenhum.
Já passei por isso. A maioria das vezes porque eu mesmo montei a primeira versão com pressa e prioridade errada.
Versionar desde o primeiro dia
Isso é o que mais me arrependo de não ter feito nos projetos antigos. Quando você sobe uma API sem versão, /usuarios e /pedidos na raiz, qualquer breaking change vira uma negociação com cada cliente consumidor. E eles nunca atualizam juntos. Nunca.
Prefiro /api/v1/ desde o primeiro endpoint. Dá trabalho? Um pouco. Mas quando você precisar lançar uma v2 com mudança de contrato, você não vai precisar convencer ninguém a migrar antes de estar pronto. Eles ficam na v1 enquanto você estabiliza a v2. Já vi empresa não conseguir lançar melhoria porque dois sistemas legados consumiam a API sem versão e ninguém sabia mais quem era o dono de um deles. A cópia de segurança do banco era a única fonte de verdade sobre o que aquele sistema fazia.
API Resources não são opcionais
Entendo a tentação de retornar o Eloquent model direto. É rápido, limpo na hora, resolve o problema do momento. Mas você está expondo a estrutura do banco de dados como contrato de API. Aí você adiciona uma coluna, renomeia outra por qualidade de código, e de repente quebrou um app mobile que estava em produção há meses.
API Resources são a camada de tradução entre o que o banco tem e o que o cliente recebe. Parecem overhead no início. Quando você vai refatorar pela segunda vez sem precisar avisar nenhum consumidor, você agradece ter feito isso desde o começo.
Aliás, isso me lembra um projeto que herdei há alguns anos. A API devolvia timestamps direto do banco em formato MySQL, aquele “2024-01-15 14:30:00” com espaço no meio. O app web manipulava assim. O app mobile também, à força. Quando o cliente pediu mudança para ISO 8601, viraram duas semanas de negociação entre três times. Com Resource, era uma linha em um lugar só.
Sanctum ou Passport?
Sanctum para a maioria dos casos. Sem hesitar.
Passport é um servidor OAuth completo. Poderoso, mas carrega toda a complexidade de OAuth: access tokens, refresh tokens, clientes, escopos. Se você está construindo uma API que vai ser consumida por terceiros que precisam de autenticação delegada, faz sentido. Se você está construindo uma API para o seu próprio frontend e seus próprios apps mobile, Sanctum resolve com muito menos configuração e muito menos superfície de ataque.
Já vi times escolherem Passport porque “parece mais enterprise”. Ficaram semanas depurando fluxo de refresh token que não precisavam nem ter implementado. Preferência pessoal: começa com Sanctum, migra para Passport se o negócio de fato exigir autenticação OAuth com terceiros.
Rate limiting e por que você vai precisar antes do que acha
Laravel tem rate limiting nativo. A maioria dos projetos não configura isso com cuidado até acontecer o primeiro problema.
E sabe o que acontece? Algum consumidor coloca a chamada da API dentro de um loop que não deveria ter loop. Um cron job que rodava de hora em hora começa a rodar de minuto em minuto por erro de configuração. Sem rate limiting, isso pressiona o banco sem aviso. Com rate limiting configurado por rota e por cliente, você recebe um 429 no log antes de perceber o problema em produção.
Um detalhe que muita gente pula: diferenciar os limites por tipo de endpoint. Endpoints de autenticação merecem um limite muito mais restritivo que endpoints de consulta. Tentativa de força bruta em /login sem rate limiting agressivo é problema de segurança, não só de performance.
Paginação: escolha um padrão e não mude mais
Parece trivial. Não é.
Tem time que pagina com page e per_page na query string, tem time que usa cursor-based, tem time que mistura os dois dependendo do endpoint. O consumidor não sabe o que vai encontrar e escreve código diferente para cada caso. Quando você decide mudar, você quebrou contratos que nem sabia que existiam.
Eu uso o padrão nativo do Laravel para a maioria dos casos, com data, links e meta na resposta. Cursor-based fica reservado para endpoints de alto volume onde paginação por offset começa a sofrer. Quando você pede a página 5000 de um resultado grande, a query com OFFSET tem custo real no banco. Mas o mais importante é ter uma decisão e seguir ela. A tecnologia específica importa menos do que a consistência.
Formato de erro que o consumidor consegue confiar
Na primeira versão de qualquer API que construí, os erros eram inconsistentes. Às vezes voltavam com a chave error, às vezes message, às vezes errors com array de validação. Dependia de onde o erro aconteceu e quem tinha escrito aquele endpoint.
O consumidor não sabe o que tratar. O desenvolvedor que mantém não sabe o que esperar.
Decida um formato no dia zero. Gosto de resposta com message, errors para detalhes de validação, e um code de aplicação que não seja só o HTTP status (porque 422 não diz nada sobre qual regra de negócio falhou). Coloca isso num handler de exceção global e qualquer erro não tratado cai nesse formato automaticamente. Simples de implementar. Caro de não ter feito desde o começo.
Caching de resposta antes de caching no servidor
HTTP tem mecanismo de cache nativo. Cache-Control, ETag, Last-Modified. Quase ninguém usa direito em API REST.
Para endpoints que retornam dados que mudam pouco, cabeçalho de cache bem configurado descarrega o servidor sem precisar de infraestrutura adicional. O cliente armazena a resposta e só bate no servidor de novo quando o cache expira ou quando o servidor sinaliza que o conteúdo mudou. É diferente de cache no servidor com Redis. Os dois têm lugar, mas são problemas distintos. Cache de resposta HTTP resolve antes de chegar no servidor. Cache no servidor resolve antes de chegar no banco.
Implementar os dois ao mesmo tempo no início é over-engineering. Começa com cache de resposta HTTP onde faz sentido, mede onde está o gargalo real, decide o próximo passo com dados.
O erro que aparece mais cedo do que parece
Documentação. Ou a falta dela.
Quando a API é pequena, todo mundo sabe o que cada endpoint faz. Quando tem cinco desenvolvedores e trinta endpoints, ninguém sabe mais. E quando tem um consumidor externo que não tem acesso ao código-fonte, você começa a receber perguntas que revelam que o endpoint nunca estava claro, nem para quem o escreveu.
OpenAPI gerado a partir das rotas do Laravel, com uma ferramenta como Scramble, resolve isso com pouco esforço. A documentação fica junto do código e é atualizada quando o código muda. Não é perfeita, mas é infinitamente melhor que um Notion desatualizado que ninguém confia mais.
Não resolvo tudo aqui. Mas versionar desde o início, separar o contrato da estrutura interna, padronizar erros e paginação, e documentar são as decisões que fazem diferença quando a API cresce. As outras aparecem com o problema específico. Essas aparecem antes de você perceber que precisava delas.
Gabriel Schunck está disponível para projetos de API, arquitetura e sistemas com Laravel. Se você está estruturando algo novo ou tentando reorganizar o que já existe, entra em contato pelo gabriels.dev.br.