> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/pt-br/architecture/backend.md).

# Backend

A API do Express 5: handlers, middleware de proteção, buscas no upstream e cache de borda.

O backend é um aplicativo Express 5. `backend-server.js` na raiz do repositório é o único arquivo que faz o roteamento; cada rota delega para exatamente um módulo de handler em `api/`. O código compartilhado do back-end fica em `common/`.

Regra geral: **`backend-server.js` decide quem pode chamar uma rota e por quanto tempo a resposta pode ficar em cache; o handler apenas busca e formata os dados.**

## Cadeia de middlewares, na ordem

```mermaid
flowchart TD
    R["Requisição recebida"] --> P["pino-http em /api  (somente quando LOG_HTTP=true)"]
    P --> RL["limitador de taxa  (somente quando SECURITY_RATE_LIMIT estiver definido)"]
    RL --> SD["desaceleração  (somente quando SECURITY_DELAY_AFTER estiver definido)"]
    SD --> J["express.json, limite de 500 kb"]
    J --> NS["Cache-Control: no-store em toda resposta /api"]
    NS --> RF["requireReferer — global em /api/*"]
    RF --> G["Guarda(s) de parâmetros por rota"]
    G --> TZ["withTimeZone() — apenas rotas de geolocalização"]
    TZ --> CA["cacheable(maxAge) — apenas em rotas que optam por isso"]
    CA --> H["Handler em api/"]
```

Vale a pena conhecer cada etapa:

1. **`pino-http`** é montado em `/api` somente quando `LOG_HTTP=true`. Ele fica antes do limitador de taxa, então respostas 429 também são registradas. Os handlers nunca escrevem linhas de "requisição recebida" por conta própria. Veja [Registro](/developer/pt-br/configuration/logging.md).
2. **Limitador de taxa** (`express-rate-limit`): uma janela de 20 minutos com `SECURITY_RATE_LIMIT` como limite; desativado quando o valor é `0` ou não está definido. Na transição exata para o estado limitado, ele escreve uma `logger.warn({ ip }, 'IP com limitação de taxa')` linha — não uma por requisição bloqueada — e opcionalmente acrescenta a um registro em disco quando `SECURITY_BLACKLIST_LOG_FILE_PATH` está definido. O IP do cliente é lido de `cf-connecting-ip`, depois a primeira `x-forwarded-for` entrada, depois `cf-connecting-ipv6`, depois `req.ip` (`trust proxy` é `1`).
3. **Desaceleração** (`express-slow-down`): uma janela de 1 hora que adiciona `acessos × 400 ms` de atraso após `SECURITY_DELAY_AFTER` requisições; desativado quando não está definido. Ambos os limitadores ignoram `/monitoring`, que tem seu próprio limitador — um túnel de telemetria com limitação silenciosamente mata o relatório de erros.
4. **`express.json({ limit: '500kb' })`** — elevado do padrão de 100 kb porque relatórios diagnósticos compartilhados chegam legitimamente a \~100 KB. Ele precisa ficar acima de `REPORT_MAX_BYTES` em `common/report-schema.js`, caso contrário os envios de relatórios morrem aqui com um 413 bruto antes da própria verificação de tamanho do handler.
5. **`no-store` padrão** em toda `/api/*` resposta.
6. **`requireReferer`**, global em `/api/*`. É a primeira coisa que uma requisição encontra após a configuração global do aplicativo, então um chamador não autorizado é barrado antes mesmo que qualquer parâmetro seja analisado.
7. **Guards de parâmetros por rota** a partir de `common/guards.js`.
8. **`withTimeZone()`** nas sete rotas de geolocalização — veja [Enriquecimento da resposta](#response-enrichment).
9. **`cacheable(maxAge)`** onde uma rota optou por isso — veja [Cache de borda](#edge-caching). Ele vem por último porque só intercepta `res.json`; nada chega até ele a menos que os guards já tenham passado, e é exatamente por isso que uma requisição rejeitada nunca pode ser armazenada em cache.
10. **O handler.**

As etapas 7 a 9 são os middlewares por rota. Elas são declaradas inline na definição da rota, sempre nessa ordem — guard, depois enriquecimento, depois cache — e o [inventário de rotas](#route-inventory-at-a-glance) abaixo lista quais rotas têm quais.

As variáveis de ambiente relacionadas à segurança estão documentadas em [Opções de segurança](/developer/pt-br/configuration/security-options.md) e [Variáveis de Ambiente](/developer/pt-br/reference/environment-variables.md).

## Validador

Controle de acesso e validação de parâmetros vivem em middleware, nunca dentro de um handler. Tudo isso está em `common/guards.js` e anexado em `backend-server.js`, então um handler pode assumir que suas entradas já estão bem formadas.

| Validador                  | Verificações                                                                                                                                                                                                                                                                                | Em caso de falha                                                                                      |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `requireReferer`           | O `Referer` o hostname é `localhost` ou esteja em `ALLOWED_DOMAINS`. Um referer que não pode ser analisado conta como negado.                                                                                                                                                               | `403` `{ error: 'Acesso negado' }`, ou `'O que você está fazendo?'` quando nenhum referer foi enviado |
| `requirePublicIP()`        | `?ip=` está presente, é um endereço IPv4/IPv6 válido e **roteável publicamente**. Espaços reservados — RFC 1918, loopback, CGNAT, link-local, documentação, multicast — são rejeitados aqui, então nenhum upstream é jamais consultado sobre um endereço para o qual ele não pode responder | `400` `Nenhum endereço IP fornecido` / `Endereço IP inválido` / `Não é um endereço IP público`        |
| `requireValidDomain()`     | `?domain=` é um domínio sintaticamente válido. **Converte para minúsculas no próprio valor** para que o cache de borda veja uma chave canônica                                                                                                                                              | `400` `Nenhum domínio fornecido` / `Domínio inválido`                                                 |
| `requireValidPrefix()`     | `?prefix=` é um CIDR bem formado. A política de quantização continua sendo responsabilidade do frontend                                                                                                                                                                                     | `400` `Nenhum prefixo fornecido` / `Prefixo inválido`                                                 |
| `requireValidASN()`        | `?asn=` é numérico, opcionalmente `AS`com prefixo -. **Reescreve para a forma numérica**                                                                                                                                                                                                    | `400` `Nenhum ASN fornecido` / `ASN inválido`                                                         |
| `requireValidCountry()`    | `?country=` tem exatamente duas letras. **Converte para maiúsculas no próprio valor** para que o cache de borda veja uma chave por país                                                                                                                                                     | `400` `Nenhum país fornecido` / `País inválido`                                                       |
| `requireValidProviderId()` | `?id=` é um slug conhecido de provedor de status de serviço                                                                                                                                                                                                                                 | `400` `Nenhum ID de provedor fornecido` / `ID de provedor inválido`                                   |
| `requireValidRecordType()` | `?type=` é um dos tipos de registro DNS que o resolvedor manipula (`common/dns-record-types.js`). **Converte para maiúsculas no próprio valor.** Sem ele, a ramificação DoH encaminharia qualquer string literalmente para resolvedores de terceiros                                        | `400` `Nenhum tipo de registro fornecido` / `Tipo de registro inválido`                               |
| `requireValidReportId()`   | O `:id` **parâmetro de rota** corresponde a 22 caracteres base64url (16 bytes aleatórios)                                                                                                                                                                                                   | `400` `ID do relatório inválido`                                                                      |

Um novo formato de parâmetro significa um novo guard em `common/guards.js` conectado em `backend-server.js` — não uma verificação inline no handler. Guards são middlewares comuns, então também se compõem além das definições de rota: `/api/cfradar` faz dispatch com base em `?view=`, e cada entrada no seu `RADAR_VIEWS` registro (`common/cf-radar.js`) declara os guards para essa view — `requireValidASN()` para `view=asn`, `requireValidCountry()` para `view=country-traffic` — que o dispatcher executa antes de tocar no upstream. Os guards são cobertos por `tests/guards.test.js`.

## Enriquecimento da resposta

Os guards leem a requisição; um middleware escreve na resposta. `withTimeZone()` (`common/ip-timezone.js`) é anexado às sete rotas de geolocalização e adiciona um `timezone` campo na saída:

```js
app.get('/api/ipsb', requirePublicIP(), withTimeZone(), cacheable(ONE_DAY_CACHE), ipsbHandler);
```

Ele intercepta `res.json` exatamente como `cacheable()` faz, e na mesma `statusCode < 400` condição, então os corpos de erro nunca recebem o campo. A zona é resolvida a partir do `latitude` / `longitude` o próprio handler retornou — nunca de uma segunda consulta do mesmo IP, que acabaria nomeando uma zona em contradição com a cidade ao lado. Coordenadas inutilizáveis produzem `''` em vez de um palpite.

Nenhum handler calcula ou encaminha um fuso horário, incluindo os repasses da API privada, então uma nova fonte de geolocalização herda o campo ao adicionar o middleware à sua rota. Detalhes em [Fontes de dados de IP](/developer/pt-br/architecture/ip-data-sources.md). Coberto por `tests/ip-timezone.test.js`.

## Formato do handler

Cada arquivo em `api/` tem uma única exportação padrão, `async (req, res) => …`, que lê `req.query` ou `req.body`, chama o upstream e escreve exatamente uma resposta. Cada arquivo começa com um comentário de cabeçalho nomeando sua rota e propósito.

A estrutura do erro é deliberadamente curta — o frontend não exibe essas strings literalmente:

```js
res.status(500).json({ error: error.message });  // falha do upstream
res.status(400).json({ error: 'Inválido …' });    // entrada inválida (geralmente um guard)
```

Alguns handlers mantêm um defensivo `req.method !== 'GET'` ramo que retorna `405` mesmo que a rota já restrinja o método, porque os testes de fumaça verificam diretamente esse ramo.

Os cinco handlers de origem de geolocalização de IP compartilham uma estrutura ainda mais enxuta: eles são construídos por `makeGeoHandler({ name, buildUrl, normalize })` fábrica em `common/geo-handler.js`, que é responsável pelo fetch, pela verificação de não-2xx, pela chamada de normalização e pelo catch uniforme de log e 500. Veja [Fontes de dados de IP](/developer/pt-br/architecture/ip-data-sources.md).

## Chamadas ao upstream

Toda chamada HTTP de saída de `api/` passa por `fetchUpstream` a partir de `common/fetch-with-timeout.js`. Nunca um `fetch()` ou `https.get()` — um provedor travado deve expirar em vez de prender a conexão.

* **timeout de 8 segundos** por padrão (o irmão do lado do navegador, `fetchWithTimeout`, usa 5 s por padrão). Ambos aceitam um `timeoutMs` substituição e encadeiam um `signal`. Timeouts aparecem como `AbortError`.
* **Um User-Agent do projeto** de `MyIP/v<version>/<VITE_SITE_URL>`, registrado na inicialização por `common/upstream-ua.js`. Alguns WAFs upstream bloqueiam rigidamente o padrão do undici `User-Agent: node`. Forks anunciam seu próprio `VITE_SITE_URL`.
* **Fornecido pelo chamador `User-Agent` os cabeçalhos sempre prevalecem**, incluindo o `{ ...req.headers }` repasso descrito abaixo.

{% hint style="info" %}
**Repasso de cabeçalhos da API IPCheck.ing.** Os handlers que fazem proxy da API privada IPCheck.ing — `ipcheck-ing`, `invisibility-test`, `update-user-achievement`, `get-user-info`, `dns-leak-test`, `persona` — encaminham os cabeçalhos do chamador para o upstream porque essa API precisa do contexto do chamador (`Accept-Language`, tokens de autenticação). Esta é uma exceção intencional. Upstreams de terceiros recebem apenas o que precisam explicitamente. `persona` é o que primeiro remove os cabeçalhos que descrevem *esta* etapa, em vez do chamador (`host`, `content-length`, `content-type`, `connection`, `transfer-encoding`): ela reserializa o corpo JSON, então o `Content-Length` já não descreve o que sai.
{% endhint %}

## Cache de borda

Toda `/api/*` resposta começa como `Cache-Control: no-store`. Rotas públicas de mudança lenta optam por isso por meio do `cacheable(maxAge)` fábrica de middleware definida em `backend-server.js`:

```js
const cacheable = (maxAge) => (req, res, next) => {
    const maxAgeSeconds = typeof maxAge === 'function' ? maxAge(req) : maxAge;
    if (maxAgeSeconds) {
        res.locals.cacheControl = `public, max-age=${maxAgeSeconds}`;
        const originalJson = res.json.bind(res);
        res.json = function (body) {
            if (res.statusCode < 400) {
                res.setHeader('Cache-Control', res.locals.cacheControl);
            }
            return originalJson(body);
        };
    }
    next();
};
```

Três consequências importam. Ele intercepta `res.json`, então o cabeçalho só chega a respostas com status abaixo de 400 — uma CDN nunca armazena uma página de erro em cache. Ele guarda o valor pretendido em `res.locals.cacheControl`, para que handlers que fazem streaming de dados binários (contornando `res.json`) possam aplicá-lo por conta própria em seu próprio caminho 2xx. E `maxAge` aceita um `(req) => seconds` resolvedor para rotas cujo TTL depende da requisição — `/api/cfradar` lê isso do registro de views, e uma resolução falsy (um `?view=`) desconhecido mantém o `no-store` padrão. Caso contrário, os handlers nunca tocam em `Cache-Control`.

Níveis de TTL atualmente em uso:

| TTL       | Rotas                                                                                                                                                    | Por que                                                                                                                                                                                                                      |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 5 minutos | `/api/service-status`, `/api/service-status/detail`                                                                                                      | Corresponde ao próprio intervalo de atualização de 5 minutos do polling em segundo plano                                                                                                                                     |
| 1 hora    | `/api/configs`, `/api/cfradar?view=outages`                                                                                                              | Flags de recurso derivadas de variáveis de ambiente mudam a cada redeploy; o feed de indisponibilidade do Radar muda numa cadência de cerca de uma hora                                                                      |
| 1 dia     | `/api/ipinfo`, `/api/ipapicom`, `/api/ipsb`, `/api/ipapiis`, `/api/ip2location`, `/api/maxmind`, `/api/whois`, `/api/github-stars`, `/api/ooni-blocking` | Dados de geolocalização e de registro quase não mudam ao longo de um dia; também mantém os limites gratuitos dos upstreams sob controle                                                                                      |
| 7 dias    | `/api/globalping-probes`                                                                                                                                 | A cobertura por país da sonda muda lentamente e os seletores falham abertos                                                                                                                                                  |
| 30 dias   | `/api/cfradar` (`view=asn`, `view=country-traffic`), `/api/asn-history`, `/api/asn-connectivity`, `/api/macchecker`                                      | Dados de registro e históricos: atribuições IEEE OUI, metadados e interconexão de ASN, histórico de roteamento BGP apenas de acréscimo — e o agregado de atividade de 28 dias do Radar, que muda no máximo em escala semanal |
| 1 ano     | `/api/map`                                                                                                                                               | Um bloco de mapa estático para uma coordenada quantizada                                                                                                                                                                     |

Os TTLs são escritos como expressões multiplicadas (`24 * 60 * 60`), não como segundos brutos.

Todo o resto permanece `no-store`: `/api/ipchecking`, `/api/dnsresolver`, `/api/dnsleaktest/session/:token`, `/api/invisibility`, `POST /api/persona/evaluate`, `/api/getuserinfo`, `PUT /api/updateuserachievement`, e ambos `/api/report` endpoints.

{% hint style="warning" %}
Nunca envolva um endpoint autenticado ou por usuário em `cacheable()`. O cache dele pertence ao upstream que controla o contexto de autenticação. Relatórios compartilhados permanecem `no-store` por um segundo motivo: um cache de borda poderia servir um relatório depois da expiração no KV, e dados diagnósticos privados não pertencem a um cache público.
{% endhint %}

## Inventário de rotas em um relance

O contrato completo está em [Endpoints da API](/developer/pt-br/reference/api-endpoints.md); esta é a visão de ligação.

| Rota                                  | Validador                                                     | Cache                      | Handler                          |
| ------------------------------------- | ------------------------------------------------------------- | -------------------------- | -------------------------------- |
| `GET /api/ipinfo`                     | `requirePublicIP()`                                           | 1 dia                      | `api/ipinfo-io.js`               |
| `GET /api/ipapicom`                   | `requirePublicIP()`                                           | 1 dia                      | `api/ipapi-com.js`               |
| `GET /api/ipapiis`                    | `requirePublicIP()`                                           | 1 dia                      | `api/ipapi-is.js`                |
| `GET /api/ip2location`                | `requirePublicIP()`                                           | 1 dia                      | `api/ip2location-io.js`          |
| `GET /api/ipsb`                       | `requirePublicIP()`                                           | 1 dia                      | `api/ip-sb.js`                   |
| `GET /api/maxmind`                    | `requirePublicIP()`                                           | 1 dia                      | `api/maxmind.js`                 |
| `GET /api/ipchecking`                 | `requirePublicIP()`                                           | no-store                   | `api/ipcheck-ing.js`             |
| `GET /api/whois`                      | —                                                             | 1 dia                      | `api/get-whois.js`               |
| `GET /api/macchecker`                 | —                                                             | 30 dias                    | `api/mac-checker.js`             |
| `GET /api/dnsresolver`                | `requireValidDomain('hostname')` + `requireValidRecordType()` | no-store                   | `api/dns-resolver.js`            |
| `GET /api/cfradar`                    | por view, de `RADAR_VIEWS` (`common/cf-radar.js`)             | por view: 30 dias / 1 hora | `api/cf-radar.js`                |
| `GET /api/asn-history`                | `requireValidPrefix()`                                        | 30 dias                    | `api/asn-history.js`             |
| `GET /api/asn-connectivity`           | `requireValidASN()`                                           | 30 dias                    | `api/asn-connectivity.js`        |
| `GET /api/ooni-blocking`              | `requireValidDomain()`                                        | 1 dia                      | `api/ooni-blocking.js`           |
| `GET /api/globalping-probes`          | —                                                             | 7 dias                     | `api/globalping-probes.js`       |
| `GET /api/service-status`             | —                                                             | 5 min                      | `api/service-status.js`          |
| `GET /api/service-status/detail`      | `requireValidProviderId()`                                    | 5 min                      | `api/service-status.js`          |
| `GET /api/map`                        | —                                                             | 1 ano                      | `api/google-map.js`              |
| `GET /api/github-stars`               | —                                                             | 1 dia                      | `api/github-stars.js`            |
| `GET /api/configs`                    | —                                                             | 1 hora                     | `api/configs.js`                 |
| `GET /api/invisibility`               | —                                                             | no-store                   | `api/invisibility-test.js`       |
| `GET /api/dnsleaktest/session/:token` | —                                                             | no-store                   | `api/dns-leak-test.js`           |
| `POST /api/persona/evaluate`          | —                                                             | no-store                   | `api/persona.js`                 |
| `GET /api/getuserinfo`                | —                                                             | no-store                   | `api/get-user-info.js`           |
| `PUT /api/updateuserachievement`      | —                                                             | no-store                   | `api/update-user-achievement.js` |
| `POST /api/report`                    | —                                                             | no-store                   | `api/share-report.js`            |
| `GET /api/report/:id`                 | `requireValidReportId()`                                      | no-store                   | `api/share-report.js`            |
| `POST /api/monitoring`                | próprio limitador de taxa                                     | no-store                   | `api/sentry-tunnel.js`           |

`/api/monitoring` está montado **único** when `VITE_SENTRY_DSN_FRONTEND` está definido. Ele usa `express.raw({ type: () => true })` — uma função coringa, porque os envelopes do Replay são binários e chegam sem nenhum `Content-Type` cabeçalho algum, o que um `'*/*'` correspondedor de strings ignoraria.

## Sequência de boot

`bootBackend()` prepara todos os conjuntos de dados offline **antes de** o listener abrir, então o servidor nunca serve um banco de dados parcialmente baixado:

1. `bootstrapMaxMindIfMissing()` depois `reloadMaxMindDatabases('startup')`
2. `bootstrapCaidaIfMissing()`
3. `bootstrapServiceStatus()`
4. Inicie o observador de arquivos do MaxMind, os autoatualizadores do MaxMind e da CAIDA e o polling de status de serviço
5. `app.listen(BACKEND_PORT)`

Cada etapa não é fatal. Uma falha deixa a API dependente degradada — o MaxMind responde `503`, as views baseadas em CAIDA retornam um grafo vazio ou fazem fallback para RIPEstat — mas nunca bloqueia a inicialização. Os detalhes dos conjuntos de dados estão em [Fontes de dados de IP](/developer/pt-br/architecture/ip-data-sources.md).

## Registro e monitoramento de erros

Arquivos do backend sempre usam o logger pino compartilhado de `common/logger.js`; uso bruto de `console.*` não é usado ali. O Pino coloca o contexto em primeiro lugar: `logger.error({ err, ip }, 'mensagem curta')`. As linhas de inicialização começam com um emoji (🚀 ouvindo, 📦 pronto, 📥 baixando, 🛡️ segurança, 🐢 limitação, 🗓️ agendamento, ⚠️ recuperável, ❌ falha); as linhas por requisição permanecem simples.

O Sentry é controlado por variável de ambiente e invisível para os handlers. `sentry-instrument.js` é carregado via `node --import` **antes de** o Express para que os hooks do carregador ESM possam auto-instrumentar o rastreamento de rotas; `backend-server.js` anexa `setupExpressErrorHandler` após todas as rotas. Sem `SENTRY_DSN_BACKEND`, `@sentry/node` nunca é carregado. Os handlers nunca importam o Sentry: exceções não capturadas e traces 5xx são automáticos, enquanto falhas capturadas ficam no logger, onde um hook espelha warn e acima para o Sentry Logs. Jobs periódicos envolvem seu tick em `common/sentry-cron.js` para check-ins, e os parâmetros de consulta da chave de API são redigidos das URLs de telemetria por `common/sentry-scrub.js`.

## Adicionando uma rota

1. Crie `api/<name>.js` com um comentário de cabeçalho e uma única exportação padrão.
2. Use `fetchUpstream` para qualquer chamada de saída.
3. Integre-o `backend-server.js`, na camada de cache correta, com as proteções de que ele precisa.
4. Se a forma do parâmetro for nova, adicione uma proteção a `common/guards.js` primeiro.
5. Adicione testes smoke a `tests/api-handlers.test.js` — bloqueio por método, ramificações de parâmetros, retornos antecipados sem chave de API. Nunca acesse um upstream real; faça asserções apenas nas ramificações que retornam antes da primeira `fetchUpstream`. Veja [Testes](/developer/pt-br/development/testing.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/pt-br/architecture/backend.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
