Do clique ao banco e de volta: o caminho de uma requisição na TOPO
Seguimos um único POST, o que cria uma atividade, do botão Salvar até o PostgreSQL e de volta, com cada número medido em staging.
- paradas na ida
- 9
- na API, mediana
- 132 ms
- resposta
- 201
Quando alguém clica em Salvar na janela de uma atividade nova, o navegador manda um pedido à nossa API e espera a resposta. Em staging, a API leva 132 ms para responder esse pedido, na mediana. Quem mede é a própria API, do começo ao fim do pedido dentro dela.
Este post segue esse pedido do começo ao fim: um POST /api/process-management/activities, que cria uma atividade num checklist. Cada número saiu de staging, o nosso ambiente de teste, e foi medido em 23 de outubro de 2026. Produção ficou de fora.
As nove paradas da ida. A seta laranja é a volta, que leva a resposta 201 até a tela.
1. O clique
A janela de atividade junta os campos do formulário e chama createActivity. Essa função faz um POST com o corpo em JSON.
Todo pedido do app passa por um cliente HTTP único, o BaseApiClient. Ele acrescenta três cabeçalhos:
Content-Type: application/jsondiz à API como ler o corpo.X-Correlation-Idé um identificador aleatório (um UUID) criado no navegador. Ele vai junto até o log da API. Com ele, a gente acha, no meio de milhares de linhas, tudo o que aconteceu por causa deste clique.X-Tenant-Slugdiz qual cliente da TOPO está em uso.
O login não vai num cabeçalho. O token de acesso fica num cookie httpOnly, que o JavaScript da página não consegue ler. Quem manda o cookie é o próprio navegador, porque o fetch sai com credentials: 'include'.
O navegador fala direto com a API. O app, feito em Next.js, é servido pela Vercel. A API, feita em NestJS, roda na Railway. Cada um tem o seu endereço, e o pedido do clique não passa pelo servidor do app.
2. DNS e TLS
Antes de mandar qualquer coisa, o navegador precisa do endereço IP da API. Quem faz essa tradução é o DNS, que funciona como uma lista telefônica de nomes. O nome da nossa API é um apelido (um registro CNAME) que aponta para a borda da Railway. A borda é a porta de entrada da rede deles.
Depois vem a conexão TCP e, em cima dela, o TLS, o cadeado do HTTPS. Quem abre o TLS é a borda da Railway, com um certificado da Let's Encrypt. Da borda para dentro, ela repassa o pedido ao nosso servidor. A resposta traz o cabeçalho HSTS, que manda o navegador usar só HTTPS com a API dali em diante.
Medimos isso com curl: 20 pedidos seguidos a uma rota de saúde da API, feitos de um computador da equipe. Os tempos são acumulados, então cada linha inclui as anteriores.
| Etapa | Mínimo | Mediana | Máximo |
|---|---|---|---|
| DNS resolvido | 3 ms | 7 ms | 217 ms |
| TCP conectado | 26 ms | 34 ms | 248 ms |
| TLS pronto | 61 ms | 125 ms | 283 ms |
| Resposta completa | 216 ms | 282 ms | 438 ms |
Os quatro máximos são do primeiro pedido, quando nada estava guardado em cache. Nos logs da Railway, cada um desses pedidos gastou 7 ms na mediana, contados da borda até a API e de volta. O resto dos 282 ms foi viagem pela rede.
3. As réplicas e o deploy
Uma réplica é uma cópia da API rodando. A configuração do deploy pede 3 réplicas em produção e 1 em staging. Quando há mais de uma, a borda reparte os pedidos entre elas. Em staging, os logs da borda confirmam a réplica única: os pedidos medidos caíram todos na mesma instância.
O deploy troca as réplicas sem derrubar pedidos:
- A versão nova sobe ao lado da antiga.
- A borda só manda tráfego para ela quando a rota de prontidão responde que sim. Essa rota testa o banco e o Redis.
- As duas versões convivem por 30 segundos.
- A antiga recebe o sinal de parar e tem 15 segundos até ser desligada. A API para de aceitar pedidos novos, dá até 10 segundos aos que estão em andamento e sai.
4. Dentro da API, antes do código da rota
O NestJS passa o pedido por camadas, sempre na mesma ordem: middlewares, guards, interceptors, validação e, por fim, a função da rota.
CORS. O app e a API estão em endereços diferentes. Por isso, antes do POST, o navegador pergunta à API, num pedido OPTIONS, se ela aceita pedidos vindos do app. A API responde que sim só para as origens da TOPO, e com cookies.
Middlewares. São funções que todo pedido atravessa. Na ordem:
- um cabeçalho que proíbe cache da resposta;
- métricas de HTTP, quando a telemetria está ligada;
- o id de correlação: a API usa o que veio do navegador ou cria um, e devolve na resposta;
- cabeçalhos de segurança, com o Helmet;
- a leitura do corpo JSON;
- compressão da resposta;
- a leitura dos cookies.
Um middleware a mais começa a contar o tempo do pedido. Quando a resposta termina, ele grava uma linha na tabela de log de requisições. A gravação fica para depois da resposta, então o log não atrasa quem pediu. É dessa tabela que saem os 132 ms do começo.
Guards. Um guard decide se o pedido segue ou para ali. Nesta rota, eles conferem:
- quem é a pessoa: o token sai do cookie e a sessão precisa estar ativa;
- se ela pode criar atividades (a permissão
PROCESS_MANAGEMENT.CREATE); - se o módulo de processos está contratado e liberado para ela;
- se o endereço do pedido bate com o cliente do token.
Sem login, a resposta é 401. Com login e sem permissão, é 403.
Interceptors. Um interceptor roda antes e depois da função da rota. Um deles guarda o cliente do token num contexto que dura o pedido inteiro, e o banco vai usá-lo lá embaixo. Outro escreve o log http_request com o id de correlação e a duração. A auditoria registra quem criou o quê numa tabela própria, sem fazer o pedido esperar pela gravação.
Validação. O corpo vira um objeto CreateActivityDto, e cada campo tem uma regra. O checklistId precisa ser um inteiro maior que zero. A descrição não pode ser vazia. Campo que o DTO não conhece é recusado. Qualquer falha devolve 400, com a mensagem em português.
5. Controller, caso de uso e repositório
O código segue a Clean Architecture, com três camadas para este pedido.
O controller é a função da rota. Ele lê o cliente do token, nunca do corpo. Depois confere se o checklist existe e pertence ao módulo, e chama o caso de uso. Em staging, os 17 pedidos que pararam aqui voltaram 404 "Checklist não encontrado", com 42 ms na mediana.
O caso de uso CreateActivity mora no pacote de aplicação e não sabe o que é HTTP. Ele:
- calcula o próximo código da atividade, quando a tela não manda um;
- cria a entidade
Activity, que valida as regras do domínio; - confere os responsáveis;
- confere se o código já existe naquele checklist e empresa;
- pede ao repositório que grave.
Ele não lança exceção para erro de negócio. Ele devolve um Result: ou ok({ id }), ou um erro com mensagem.
O repositório PrismaActivityRepository mora no pacote de infraestrutura e fala com o banco pelo Prisma. Um erro do Prisma vira mensagem: chave única repetida, por exemplo, vira "Registro já existe".
6. O pool de conexões e o RLS
Abrir uma conexão com o PostgreSQL custa caro: tem rede, TLS e login. Por isso a API mantém um pool, um conjunto de conexões já abertas que os pedidos pegam emprestadas e devolvem.
O pool é o pg, ligado ao Prisma pelo adaptador @prisma/adapter-pg. O tamanho vem da URL do banco. Sem ele, a conta é o número de núcleos físicos vezes 2, mais 1. Em staging, a URL não diz o tamanho. Rodamos a mesma função dentro do contêiner da API, e ela respondeu 49 conexões. O PostgreSQL de staging aceita até 500. Na hora da medida, a API segurava 2 conexões, as duas paradas. Um pedido espera no máximo 10 segundos por uma conexão livre.
O banco guarda os dados de todos os clientes nas mesmas tabelas. Quem separa um cliente do outro é o RLS (Row-Level Security), uma regra do próprio PostgreSQL. Cada operação do Prisma vira uma transação curta:
BEGIN;
SELECT set_config('app.current_tenant', '<cliente do token>', true);
-- a consulta ou a gravação da vez
COMMIT;
O true faz a marca valer só dentro daquela transação. A conexão volta ao pool limpa. Se não há cliente no contexto, a API recusa a operação antes de chegar ao banco.
Do lado do banco, cada tabela de cliente tem a política ligada com FORCE, que vale até para o dono da tabela. A regra compara a coluna tenant_id com a marca da transação. Simplificada, fica assim:
tenant_id = current_setting('app.current_tenant')::integer
A API entra no banco com um usuário sem poder de administrador e sem a permissão que pula o RLS. Num INSERT, um gatilho preenche o tenant_id com o cliente da transação.
7. Uma consulta real
Antes de gravar, o caso de uso procura uma atividade com o mesmo código no checklist e na empresa. Rodamos essa consulta em staging com EXPLAIN (ANALYZE, BUFFERS), que executa a consulta e mostra o plano. Foi numa transação só de leitura, com o usuário da API e o cliente marcado, como a API faz. Este é um trecho do plano, com os valores trocados por parâmetros:
Limit (actual time=2.060..2.063 rows=1)
-> Sort (Sort Key: code)
-> Bitmap Heap Scan on activities (rows=1)
Filter: is_current AND tenant_id = $cliente
AND <política de RLS> AND code ILIKE $codigo
Rows Removed by Filter: 81
-> Bitmap Index Scan on <índice único> (rows=82)
Index Cond: company_id = $empresa AND checklist_id = $checklist
Execution Time: 2.099 ms
O índice achou 82 atividades daquela empresa no checklist. O filtro ficou com 1. A consulta levou 2,1 ms e usou 46 páginas de 8 kB, 4 delas de fora do cache do PostgreSQL. A política de RLS aparece no filtro, e ninguém a escreveu na consulta: o PostgreSQL a acrescentou sozinho.
A outra consulta do caso de uso, que lista as atividades do checklist para calcular o próximo código, trouxe 574 linhas em 0,97 ms, todas do cache do PostgreSQL.
E se dois pedidos chegarem juntos com o mesmo código? Os dois podem passar pela conferência acima. Mas o índice é único, então o banco recusa a segunda gravação. O Prisma avisa com o erro P2002, e a API responde 409.
8. A volta
O caso de uso devolve ok({ id }). O controller desembrulha o Result, e o NestJS transforma o objeto em JSON:
{ "id": 123 }
O status é 201 Created, o padrão do NestJS para POST. A rota não troca o status.
Quando o Result é um erro, o controller escolhe o status pelo código do erro e, se não houver código, pela mensagem. O corpo do erro tem sempre a mesma forma, { "success": false, "error": "<mensagem em português>" }. Erro inesperado devolve 500 com uma mensagem genérica e um código de referência, que é o id de correlação. O detalhe técnico fica só no nosso log.
| Status e quando | Em staging, 30 dias |
|---|---|
| 200 OK: leitura ou alteração que deu certo | 129.593 |
| 201 Created: criação, como esta atividade | 4.458 |
| 204 No Content: resposta sem corpo, como a de uma exclusão que deu certo | 9.412 |
| 400 Bad Request: corpo inválido, ou erro de negócio sem status próprio | 579 |
| 401 Unauthorized: sem login, ou com o token vencido | 6.929 |
| 403 Forbidden: com login, mas sem permissão ou sem o módulo | 967 |
| 404 Not Found: o recurso não existe para este cliente | 780 |
| 409 Conflict: já existe uma atividade com este código | 43 |
| 422 Unprocessable Entity: o pedido está certo, mas a regra recusa (enviar para revisão sem a evidência obrigatória) | 3 |
| 500 Internal Server Error: erro inesperado | 2 |
A contagem é da tabela de log de requisições de staging, do dia 23 de setembro ao dia 23 de outubro de 2026, somando todas as rotas.
O tempo da volta já está nos números do começo. Nos 125 POST que criaram atividade em staging entre 28 de setembro e 19 de outubro, a API levou 132 ms na mediana. Em 95% deles, levou até 293 ms.
9. O que a tela faz com a resposta
O BaseApiClient lê o JSON. Se a resposta fosse 204, ele devolveria um objeto vazio, porque não há corpo.
Com o 201, a janela mostra o aviso "Atividade criada com sucesso", recarrega a lista de atividades do checklist e fecha. Quando a pessoa escolhe várias empresas, a tela manda um POST por empresa, em paralelo, e o aviso diz em quais deu certo e em quais falhou.
Os erros seguem outro caminho:
- 401: o cliente tenta renovar o token uma vez e repete o pedido. Se não der, o erro pede que a pessoa entre de novo.
- 403: vira um erro de autorização.
- Os outros: viram um erro com o status e a mensagem da API.
A janela mostra essa mensagem num aviso vermelho. É a mesma frase em português que a API escreveu.
O que os números dizem
Do computador da equipe, um pedido simples levou 282 ms, e só 7 ms foram do lado da Railway, entre a borda e a API. Num POST que cria atividade, a API leva 132 ms na mediana: guards, validação, as consultas de conferência, a gravação e o log. A consulta que a gente olhou de perto, no banco, levou 2,1 ms.