<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ApiTraffic</title>
    <description>The latest articles on DEV Community by ApiTraffic (@apitraffic).</description>
    <link>https://dev.to/apitraffic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4206743%2Fc2b89a7c-ab9e-46c8-9c20-e0b842a0ad76.png</url>
      <title>DEV Community: ApiTraffic</title>
      <link>https://dev.to/apitraffic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apitraffic"/>
    <language>en</language>
    <item>
      <title>What small Node teams should log in the AI-attack era</title>
      <dc:creator>ApiTraffic</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:00:26 +0000</pubDate>
      <link>https://dev.to/apitraffic/what-small-node-teams-should-log-in-the-ai-attack-era-18lb</link>
      <guid>https://dev.to/apitraffic/what-small-node-teams-should-log-in-the-ai-attack-era-18lb</guid>
      <description>&lt;h1&gt;
  
  
  What small Node teams should log in the AI-attack era
&lt;/h1&gt;

&lt;p&gt;Your API's traffic isn't mostly human anymore. Here's what to log so you can tell the difference.&lt;/p&gt;

&lt;p&gt;Imperva's 2026 Bad Bot Report puts bots at more than 53% of all web traffic, with 40% classified as malicious. It also reports that 27% of attacks now target APIs and identity systems, and that AI-driven bot attacks grew 12.5x. AI tooling makes recon, scraping and credential stuffing cheap to write and cheap to run. Scanners don't check your headcount before they send requests. A small team just has less time to look at what arrives.&lt;/p&gt;

&lt;p&gt;You don't need a security team to see this traffic. You need the right fields on every request, and a few patterns you know how to spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What your host logs miss
&lt;/h2&gt;

&lt;p&gt;The default logs on Render, Railway, Fly or a bare VPS usually give you one line per request: timestamp, method, path, status, maybe latency. That's enough to see that something got slow. It's not enough to answer the questions an attack raises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who was calling?&lt;/strong&gt; Not just the IP, but which user or API key, if any.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What did they send?&lt;/strong&gt; The request body is where injection attempts and bad payloads live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What else did they do?&lt;/strong&gt; One request to &lt;code&gt;/v1/users/1042&lt;/code&gt; is normal. Four hundred requests to &lt;code&gt;/v1/users/1&lt;/code&gt; through &lt;code&gt;/v1/users/400&lt;/code&gt;, from one caller, in two minutes, is not. You only see that if you can group requests by caller and look at them in order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Host logs answer the first question partially and the other two not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The minimum to capture for every request
&lt;/h2&gt;

&lt;p&gt;If you log nothing else, log these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;method&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;What was asked for. Keep the raw path &lt;em&gt;and&lt;/em&gt; the route template (&lt;code&gt;/v1/users/:id&lt;/code&gt;) so you can group.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401, 403, 404 and 429 clusters are the earliest signs of probing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;latency_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scrapers and expensive queries show up as load before they show up as errors.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The caller's address, correctly resolved behind your proxy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;user_id&lt;/code&gt; or API key ID&lt;/td&gt;
&lt;td&gt;Turns "an IP" into "an account", which is what you'll actually act on. Log an ID, never the token.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Easy to fake, but useful for grouping. A lot of automation doesn't bother to fake it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;request_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oversized bodies and bulk-export requests stand out.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ASN (the network an IP belongs to) is worth having too, if your edge (Cloudflare, a load balancer) adds it as a header. A sudden wave from a hosting provider your users never come from is a strong signal. You don't need to resolve it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Five patterns to watch
&lt;/h2&gt;

&lt;p&gt;All of these are cheap to detect once the fields above are in one searchable place. The log lines below are illustrative, using documentation IP ranges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Login bursts.&lt;/strong&gt; Credential stuffing looks like a lot of &lt;code&gt;POST /v1/auth/login&lt;/code&gt; with &lt;code&gt;401&lt;/code&gt;s from a small set of IPs, or a slow trickle from many.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;203.0.113.24  POST /v1/auth/login  401  38ms
203.0.113.24  POST /v1/auth/login  401  41ms
203.0.113.24  POST /v1/auth/login  401  36ms   (x 412 in 10 min)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sequential ID walking.&lt;/strong&gt; One caller requesting &lt;code&gt;/v1/users/1&lt;/code&gt;, &lt;code&gt;/2&lt;/code&gt;, &lt;code&gt;/3&lt;/code&gt;... This is how Broken Object Level Authorization gets found and exploited, and it's #1 on the OWASP API Security Top 10 (2023). If the responses are &lt;code&gt;200&lt;/code&gt;, you have a data leak, not a probe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;198.51.100.7  GET /v1/users/1041  200
198.51.100.7  GET /v1/users/1042  200
198.51.100.7  GET /v1/users/1043  200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pagination scraping.&lt;/strong&gt; &lt;code&gt;GET /v1/products?page=1&lt;/code&gt; through &lt;code&gt;page=900&lt;/code&gt; at a steady rate, often with a generic or missing user agent. It's not an exploit, but it costs you compute and can lift your whole catalogue. OWASP lists this class of problem as Unrestricted Resource Consumption (API4:2023).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;404 sweeps.&lt;/strong&gt; Requests for paths you never built: &lt;code&gt;/.env&lt;/code&gt;, &lt;code&gt;/.git/config&lt;/code&gt;, &lt;code&gt;/admin&lt;/code&gt;, &lt;code&gt;/wp-login.php&lt;/code&gt;. Almost always automated. Mostly harmless on a Node API, until one of them returns &lt;code&gt;200&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;192.0.2.91  GET /.env            404
192.0.2.91  GET /.git/config     404
192.0.2.91  GET /admin           404
192.0.2.91  GET /api/v1/.env     404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;New networks.&lt;/strong&gt; Traffic that suddenly arrives from an ASN or country your users have never come from, often a cloud provider. On its own it proves nothing. Combined with any pattern above, it's the caller to look at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. When to block and when to watch
&lt;/h2&gt;

&lt;p&gt;Not everything that looks automated should be blocked. Some of it is your own monitoring, a partner integration, or a search engine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch first.&lt;/strong&gt; Log and alert on the patterns above for a week before you block anything. You'll find the legitimate automation you forgot about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit sensitive routes.&lt;/strong&gt; Login, password reset, signup and anything that sends email or SMS. Ten attempts per IP per minute on login is a reasonable start, and it's the example ApiTraffic's own docs use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block what's never legitimate.&lt;/strong&gt; Paths like &lt;code&gt;/.env&lt;/code&gt; on an API that has no such file. IPs you've confirmed are abusive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow-list what must never break.&lt;/strong&gt; Health checks, your own services, known partners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't zero bot traffic. It's knowing which bots you have, and making the expensive ones pay a &lt;code&gt;429&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Do it yourself in Express
&lt;/h2&gt;

&lt;p&gt;Here's a minimal version: structured logs with the fields above, a stricter rate limit on login, and a cheap 404-sweep block. It assumes &lt;code&gt;express-rate-limit&lt;/code&gt; and that your app runs behind one proxy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rateLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express-rate-limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trust proxy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// trust exactly one proxy hop, so req.ip is the real client&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100kb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// 1. One structured log line per request&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// set by your auth middleware&lt;/span&gt;
      &lt;span class="na"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;req_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;asn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-asn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// only if your edge sets and overwrites it&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Block paths that are never legitimate on this API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/(\.&lt;/span&gt;&lt;span class="sr"&gt;env|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;git|wp-login&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;php|phpmyadmin&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Tighter limit on login&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your log drain somewhere searchable and write the queries: 401s on login grouped by IP, distinct IDs per caller on &lt;code&gt;/v1/users/:id&lt;/code&gt;, 404s per IP per hour.&lt;/p&gt;

&lt;p&gt;Two things to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;trust proxy&lt;/code&gt;.&lt;/strong&gt; If you trust every hop, a caller can write any IP into &lt;code&gt;X-Forwarded-For&lt;/code&gt; and your logs and rate limits will believe it. Trust only the proxies you actually run behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The body.&lt;/strong&gt; This logger leaves bodies out on purpose. If you add them, redact first (see section 7).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works. The cost is that you now maintain a logger, a log pipeline, the queries and the alerting, and the rate limit counts per process unless you add a shared store.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Or do it with ApiTraffic
&lt;/h2&gt;

&lt;p&gt;ApiTraffic captures every request and response from one middleware, and the same SDK carries a firewall. The quickstart takes about five minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @apitraffic/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apitraffic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@apitraffic/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apitraffic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// after body parsers, before routes&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads &lt;code&gt;API_TRAFFIC_TOKEN&lt;/code&gt; and &lt;code&gt;API_TRAFFIC_BUCKET&lt;/code&gt; from the environment. To tie requests to accounts, tag them in a route or your auth middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;apitraffic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The patterns above become searches in the request log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;url_path == "/v1/auth/login" AND status_code == 401
status_code == 404 AND url_path like "/.env*"
tag:userId == "1042" AND status_code &amp;gt;= 400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the blocking side lives in a firewall policy, pushed to your servers without a deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; per IP, header (an API key, say) or path. Over the limit gets a &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access lists&lt;/strong&gt; to allow or deny by IP or CIDR, country, ASN (from a header your edge sets), user agent or path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioral protection&lt;/strong&gt; that scores each caller over a window on exactly these signals: distinct object IDs per endpoint, 401/403 counts, 404 counts and distinct endpoints. Callers who cross the threshold get a temporary deny on every server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor mode&lt;/strong&gt; first, so you can see what a policy &lt;em&gt;would&lt;/em&gt; block before it blocks anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Redact before it leaves your server
&lt;/h2&gt;

&lt;p&gt;Capturing more detail means capturing more you shouldn't keep: tokens, passwords, emails, card numbers.&lt;/p&gt;

&lt;p&gt;If you roll your own logger, never log raw &lt;code&gt;Authorization&lt;/code&gt; or &lt;code&gt;Cookie&lt;/code&gt; headers, strip password and token fields from bodies before you write them, and keep body logging off for routes that handle credentials.&lt;/p&gt;

&lt;p&gt;In ApiTraffic, credential headers such as &lt;code&gt;Authorization&lt;/code&gt; are always masked. Redaction rules set in the dashboard can run at the API, inside your process, so the value never leaves your server. They're pushed to connected servers when you save, with no deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Log method, path, status, latency, IP, user ID, user agent and size on every request. Watch login bursts, ID walking, pagination scraping, 404 sweeps and new networks. Watch before you block. Rate-limit what's sensitive.&lt;/p&gt;

&lt;h1&gt;
  
  
  What small Node teams should log in the AI-attack era
&lt;/h1&gt;

&lt;p&gt;Your API's traffic isn't mostly human anymore. Here's what to log so you can tell the difference.&lt;/p&gt;

&lt;p&gt;Imperva's 2026 Bad Bot Report puts bots at more than 53% of all web traffic, with 40% classified as malicious. It also reports that 27% of attacks now target APIs and identity systems, and that AI-driven bot attacks grew 12.5x. AI tooling makes recon, scraping and credential stuffing cheap to write and cheap to run. Scanners don't check your headcount before they send requests. A small team just has less time to look at what arrives.&lt;/p&gt;

&lt;p&gt;You don't need a security team to see this traffic. You need the right fields on every request, and a few patterns you know how to spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What your host logs miss
&lt;/h2&gt;

&lt;p&gt;The default logs on Render, Railway, Fly or a bare VPS usually give you one line per request: timestamp, method, path, status, maybe latency. That's enough to see that something got slow. It's not enough to answer the questions an attack raises:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who was calling?&lt;/strong&gt; Not just the IP, but which user or API key, if any.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What did they send?&lt;/strong&gt; The request body is where injection attempts and bad payloads live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What else did they do?&lt;/strong&gt; One request to &lt;code&gt;/v1/users/1042&lt;/code&gt; is normal. Four hundred requests to &lt;code&gt;/v1/users/1&lt;/code&gt; through &lt;code&gt;/v1/users/400&lt;/code&gt;, from one caller, in two minutes, is not. You only see that if you can group requests by caller and look at them in order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Host logs answer the first question partially and the other two not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The minimum to capture for every request
&lt;/h2&gt;

&lt;p&gt;If you log nothing else, log these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;method&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;What was asked for. Keep the raw path &lt;em&gt;and&lt;/em&gt; the route template (&lt;code&gt;/v1/users/:id&lt;/code&gt;) so you can group.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401, 403, 404 and 429 clusters are the earliest signs of probing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;latency_ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scrapers and expensive queries show up as load before they show up as errors.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The caller's address, correctly resolved behind your proxy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;user_id&lt;/code&gt; or API key ID&lt;/td&gt;
&lt;td&gt;Turns "an IP" into "an account", which is what you'll actually act on. Log an ID, never the token.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Easy to fake, but useful for grouping. A lot of automation doesn't bother to fake it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;request_size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oversized bodies and bulk-export requests stand out.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ASN (the network an IP belongs to) is worth having too, if your edge (Cloudflare, a load balancer) adds it as a header. A sudden wave from a hosting provider your users never come from is a strong signal. You don't need to resolve it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Five patterns to watch
&lt;/h2&gt;

&lt;p&gt;All of these are cheap to detect once the fields above are in one searchable place. The log lines below are illustrative, using documentation IP ranges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Login bursts.&lt;/strong&gt; Credential stuffing looks like a lot of &lt;code&gt;POST /v1/auth/login&lt;/code&gt; with &lt;code&gt;401&lt;/code&gt;s from a small set of IPs, or a slow trickle from many.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;203.0.113.24  POST /v1/auth/login  401  38ms
203.0.113.24  POST /v1/auth/login  401  41ms
203.0.113.24  POST /v1/auth/login  401  36ms   (x 412 in 10 min)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sequential ID walking.&lt;/strong&gt; One caller requesting &lt;code&gt;/v1/users/1&lt;/code&gt;, &lt;code&gt;/2&lt;/code&gt;, &lt;code&gt;/3&lt;/code&gt;... This is how Broken Object Level Authorization gets found and exploited, and it's #1 on the OWASP API Security Top 10 (2023). If the responses are &lt;code&gt;200&lt;/code&gt;, you have a data leak, not a probe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;198.51.100.7  GET /v1/users/1041  200
198.51.100.7  GET /v1/users/1042  200
198.51.100.7  GET /v1/users/1043  200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pagination scraping.&lt;/strong&gt; &lt;code&gt;GET /v1/products?page=1&lt;/code&gt; through &lt;code&gt;page=900&lt;/code&gt; at a steady rate, often with a generic or missing user agent. It's not an exploit, but it costs you compute and can lift your whole catalogue. OWASP lists this class of problem as Unrestricted Resource Consumption (API4:2023).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;404 sweeps.&lt;/strong&gt; Requests for paths you never built: &lt;code&gt;/.env&lt;/code&gt;, &lt;code&gt;/.git/config&lt;/code&gt;, &lt;code&gt;/admin&lt;/code&gt;, &lt;code&gt;/wp-login.php&lt;/code&gt;. Almost always automated. Mostly harmless on a Node API, until one of them returns &lt;code&gt;200&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;192.0.2.91  GET /.env            404
192.0.2.91  GET /.git/config     404
192.0.2.91  GET /admin           404
192.0.2.91  GET /api/v1/.env     404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;New networks.&lt;/strong&gt; Traffic that suddenly arrives from an ASN or country your users have never come from, often a cloud provider. On its own it proves nothing. Combined with any pattern above, it's the caller to look at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. When to block and when to watch
&lt;/h2&gt;

&lt;p&gt;Not everything that looks automated should be blocked. Some of it is your own monitoring, a partner integration, or a search engine.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watch first.&lt;/strong&gt; Log and alert on the patterns above for a week before you block anything. You'll find the legitimate automation you forgot about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit sensitive routes.&lt;/strong&gt; Login, password reset, signup and anything that sends email or SMS. Ten attempts per IP per minute on login is a reasonable start, and it's the example ApiTraffic's own docs use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block what's never legitimate.&lt;/strong&gt; Paths like &lt;code&gt;/.env&lt;/code&gt; on an API that has no such file. IPs you've confirmed are abusive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow-list what must never break.&lt;/strong&gt; Health checks, your own services, known partners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't zero bot traffic. It's knowing which bots you have, and making the expensive ones pay a &lt;code&gt;429&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Do it yourself in Express
&lt;/h2&gt;

&lt;p&gt;Here's a minimal version: structured logs with the fields above, a stricter rate limit on login, and a cheap 404-sweep block. It assumes &lt;code&gt;express-rate-limit&lt;/code&gt; and that your app runs behind one proxy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rateLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express-rate-limit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trust proxy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// trust exactly one proxy hop, so req.ip is the real client&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100kb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// 1. One structured log line per request&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hrtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// set by your auth middleware&lt;/span&gt;
      &lt;span class="na"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;req_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;asn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-asn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// only if your edge sets and overwrites it&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Block paths that are never legitimate on this API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/(\.&lt;/span&gt;&lt;span class="sr"&gt;env|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;git|wp-login&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;php|phpmyadmin&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Tighter limit on login&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your log drain somewhere searchable and write the queries: 401s on login grouped by IP, distinct IDs per caller on &lt;code&gt;/v1/users/:id&lt;/code&gt;, 404s per IP per hour.&lt;/p&gt;

&lt;p&gt;Two things to get right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;trust proxy&lt;/code&gt;.&lt;/strong&gt; If you trust every hop, a caller can write any IP into &lt;code&gt;X-Forwarded-For&lt;/code&gt; and your logs and rate limits will believe it. Trust only the proxies you actually run behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The body.&lt;/strong&gt; This logger leaves bodies out on purpose. If you add them, redact first (see section 7).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works. The cost is that you now maintain a logger, a log pipeline, the queries and the alerting, and the rate limit counts per process unless you add a shared store.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Or do it with ApiTraffic
&lt;/h2&gt;

&lt;p&gt;ApiTraffic captures every request and response from one middleware, and the same SDK carries a firewall. The quickstart takes about five minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @apitraffic/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apitraffic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@apitraffic/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apitraffic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// after body parsers, before routes&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;apiRouter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads &lt;code&gt;API_TRAFFIC_TOKEN&lt;/code&gt; and &lt;code&gt;API_TRAFFIC_BUCKET&lt;/code&gt; from the environment. To tie requests to accounts, tag them in a route or your auth middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;apitraffic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The patterns above become searches in the request log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;url_path == "/v1/auth/login" AND status_code == 401
status_code == 404 AND url_path like "/.env*"
tag:userId == "1042" AND status_code &amp;gt;= 400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the blocking side lives in a firewall policy, pushed to your servers without a deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; per IP, header (an API key, say) or path. Over the limit gets a &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access lists&lt;/strong&gt; to allow or deny by IP or CIDR, country, ASN (from a header your edge sets), user agent or path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioral protection&lt;/strong&gt; that scores each caller over a window on exactly these signals: distinct object IDs per endpoint, 401/403 counts, 404 counts and distinct endpoints. Callers who cross the threshold get a temporary deny on every server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor mode&lt;/strong&gt; first, so you can see what a policy &lt;em&gt;would&lt;/em&gt; block before it blocks anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Redact before it leaves your server
&lt;/h2&gt;

&lt;p&gt;Capturing more detail means capturing more you shouldn't keep: tokens, passwords, emails, card numbers.&lt;/p&gt;

&lt;p&gt;If you roll your own logger, never log raw &lt;code&gt;Authorization&lt;/code&gt; or &lt;code&gt;Cookie&lt;/code&gt; headers, strip password and token fields from bodies before you write them, and keep body logging off for routes that handle credentials.&lt;/p&gt;

&lt;p&gt;In ApiTraffic, credential headers such as &lt;code&gt;Authorization&lt;/code&gt; are always masked. Redaction rules set in the dashboard can run at the API, inside your process, so the value never leaves your server. They're pushed to connected servers when you save, with no deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Log method, path, status, latency, IP, user ID, user agent and size on every request. Watch login bursts, ID walking, pagination scraping, 404 sweeps and new networks. Watch before you block. Rate-limit what's sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start free and see the last hour of what's hitting your API: &lt;a href="https://apitraffic.io?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=ai-era&amp;amp;utm_content=p03" rel="noopener noreferrer"&gt;apitraffic.io&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>node</category>
      <category>aiops</category>
    </item>
  </channel>
</rss>
