TOPOengenharia
← planta do sistemaRead in English

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
peça no sistema
API
peça na esteira
Produção
  • api
  • nestjs
  • postgresql
  • rls
  • desempenho

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.

O caminho de um POST que cria uma atividade: navegador, DNS, borda da Railway, middlewares, guards, interceptors e validação, controller, caso de uso e repositório, pool de conexões e PostgreSQL, e a volta com 201

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/json diz à 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-Slug diz 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.

EtapaMínimoMedianaMáximo
DNS resolvido3 ms7 ms217 ms
TCP conectado26 ms34 ms248 ms
TLS pronto61 ms125 ms283 ms
Resposta completa216 ms282 ms438 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:

  1. A versão nova sobe ao lado da antiga.
  2. 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.
  3. As duas versões convivem por 30 segundos.
  4. 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:

  1. um cabeçalho que proíbe cache da resposta;
  2. métricas de HTTP, quando a telemetria está ligada;
  3. o id de correlação: a API usa o que veio do navegador ou cria um, e devolve na resposta;
  4. cabeçalhos de segurança, com o Helmet;
  5. a leitura do corpo JSON;
  6. compressão da resposta;
  7. 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:

  1. calcula o próximo código da atividade, quando a tela não manda um;
  2. cria a entidade Activity, que valida as regras do domínio;
  3. confere os responsáveis;
  4. confere se o código já existe naquele checklist e empresa;
  5. 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 quandoEm staging, 30 dias
200 OK: leitura ou alteração que deu certo129.593
201 Created: criação, como esta atividade4.458
204 No Content: resposta sem corpo, como a de uma exclusão que deu certo9.412
400 Bad Request: corpo inválido, ou erro de negócio sem status próprio579
401 Unauthorized: sem login, ou com o token vencido6.929
403 Forbidden: com login, mas sem permissão ou sem o módulo967
404 Not Found: o recurso não existe para este cliente780
409 Conflict: já existe uma atividade com este código43
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 inesperado2

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.