Secure Your Next.js App with Arcjet: Rate Limiting, Bot Protection, and Shield WAF in 2026

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

Shipping a Next.js app in 2026 without a security layer is asking for trouble. Scraper bots, credential stuffing, API abuse, and AI prompt-injection attacks are cheap, automated, and relentless. Writing bespoke middleware for each of these is slow and fragile.

Arcjet is a developer-first security SDK that runs inside your Next.js app. One package, one protect() call, and you get rate limiting, bot detection, a WAF (Shield), email validation, PII filtering, and prompt-injection detection — all as code, all testable, all with granular per-route rules.

In this tutorial you will add all six protections to a Next.js 15 App Router project, test them locally, and deploy safely to production. No DNS reconfiguration, no reverse proxy, no additional infrastructure.

What You'll Build

A Next.js 15 app with:

  • A public /api/hello endpoint protected by Shield WAF, bot detection, and fixed-window rate limiting.
  • A /api/signup endpoint that blocks disposable emails and limits signups per IP.
  • An /api/chat AI endpoint that enforces a token budget, blocks prompt-injection, and masks sensitive info.
  • A dashboard of real-time security decisions in the Arcjet console.

Prerequisites

Before starting, ensure you have:

  • Node.js 20 or newer installed
  • A Next.js 15 project using the App Router (or follow Step 1 to create one)
  • An Arcjet account — the free tier covers this tutorial
  • Familiarity with TypeScript and Next.js route handlers
  • Basic understanding of HTTP status codes and middleware

Step 1: Create a Next.js 15 Project

Spin up a fresh project (skip this if you already have one).

npx create-next-app@latest nextjs-arcjet-demo \
  --typescript --app --tailwind --eslint \
  --src-dir --import-alias "@/*"
 
cd nextjs-arcjet-demo

Start the dev server once to confirm it boots.

npm run dev

Open http://localhost:3000 — you should see the default Next.js landing page.

Step 2: Install Arcjet

Install the Next.js SDK.

npm install @arcjet/next

Then install the optional @arcjet/inspect helper (useful for verifying spoofed bots).

npm install @arcjet/inspect

Step 3: Get Your Arcjet Site Key

Sign in at app.arcjet.com, create a site, and copy the site key (starts with ajkey_).

Add it to .env.local.

# .env.local
ARCJET_KEY=ajkey_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Arcjet is edge-safe: the key stays on the server and never ships to the browser.

Step 4: Protect a Public API Route

Create src/app/api/hello/route.ts.

// src/app/api/hello/route.ts
import arcjet, { detectBot, shield, fixedWindow } from "@arcjet/next";
import { NextResponse } from "next/server";
 
const aj = arcjet({
  key: process.env.ARCJET_KEY!,
  rules: [
    shield({ mode: "LIVE" }),
    detectBot({
      mode: "LIVE",
      allow: [
        "CATEGORY:SEARCH_ENGINE",
        "CATEGORY:MONITOR",
      ],
    }),
    fixedWindow({
      mode: "LIVE",
      window: "1m",
      max: 20,
    }),
  ],
});
 
export async function GET(req: Request) {
  const decision = await aj.protect(req);
 
  if (decision.isDenied()) {
    if (decision.reason.isRateLimit()) {
      return NextResponse.json(
        { error: "Too many requests" },
        { status: 429 },
      );
    }
    if (decision.reason.isBot()) {
      return NextResponse.json(
        { error: "Automated access is not allowed" },
        { status: 403 },
      );
    }
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }
 
  return NextResponse.json({ message: "Hello, human." });
}

Three rules fire on every request:

  • Shield blocks OWASP Top 10 style attacks (SQL injection, path traversal, XSS payloads in query strings).
  • detectBot allows verified search engines and monitoring services but blocks everything else, including curl, Puppeteer, and headless Chromium.
  • fixedWindow caps each client at 20 requests per minute.

Test it from two terminals.

# Terminal 1 — follow decisions in your Arcjet dashboard
# Terminal 2 — flood the endpoint
for i in {1..25}; do curl -s -o /dev/null -w "%{http_code}\n" \
  http://localhost:3000/api/hello; done

You should see roughly 20 responses with status 200, then 429 for the rest.

Step 5: Block Bots but Allow Real Browsers

Arcjet ships an opinionated bot taxonomy. A partial list:

CategoryExample clients
CATEGORY:SEARCH_ENGINEGooglebot, Bingbot, Yandex
CATEGORY:MONITORUptime Robot, Pingdom, BetterStack
CATEGORY:PREVIEWSlackbot, Discordbot, Twitterbot
CATEGORY:AIGPTBot, ClaudeBot, PerplexityBot

To allow link previews on social platforms but block AI crawlers, adjust the rule.

detectBot({
  mode: "LIVE",
  allow: [
    "CATEGORY:SEARCH_ENGINE",
    "CATEGORY:MONITOR",
    "CATEGORY:PREVIEW",
  ],
  // deny: ["CATEGORY:AI"],  // explicit AI block (optional)
}),

Some bots lie about their User-Agent. Use isSpoofedBot to cross-check the request IP against the claimed bot's known IP ranges.

import { isSpoofedBot } from "@arcjet/inspect";
 
const decision = await aj.protect(req);
 
if (decision.results.some(isSpoofedBot)) {
  return NextResponse.json(
    { error: "Spoofed bot detected" },
    { status: 403 },
  );
}

Step 6: Protect a Signup Form with protectSignup

Signup endpoints are a prime target: fake accounts, credential stuffing, and spam lists. Arcjet bundles three rules into one helper.

Create src/app/api/signup/route.ts.

// src/app/api/signup/route.ts
import arcjet, { protectSignup } from "@arcjet/next";
import { NextResponse } from "next/server";
 
const aj = arcjet({
  key: process.env.ARCJET_KEY!,
  rules: [
    protectSignup({
      rateLimit: {
        mode: "LIVE",
        interval: "10m",
        max: 5,
      },
      bots: {
        mode: "LIVE",
        allow: [],
      },
      email: {
        mode: "LIVE",
        deny: ["DISPOSABLE", "INVALID", "NO_MX_RECORDS"],
      },
    }),
  ],
});
 
export async function POST(req: Request) {
  const body = await req.json();
  const email = String(body.email ?? "");
 
  const decision = await aj.protect(req, { email });
 
  if (decision.isDenied()) {
    if (decision.reason.isEmail()) {
      return NextResponse.json(
        { error: "Invalid or disposable email" },
        { status: 400 },
      );
    }
    if (decision.reason.isRateLimit()) {
      return NextResponse.json(
        { error: "Too many signup attempts" },
        { status: 429 },
      );
    }
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }
 
  return NextResponse.json({ ok: true });
}

Test with a disposable address.

curl -X POST http://localhost:3000/api/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"throwaway@mailinator.com"}'
# → 400 Invalid or disposable email
 
curl -X POST http://localhost:3000/api/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"real@noqta.tn"}'
# → 200 ok

The email rule checks three layers: syntactic validity, MX record presence, and a disposable-domain list maintained by Arcjet.

Step 7: Protect an AI Chat Endpoint

AI endpoints are expensive and uniquely vulnerable. A single malicious request can burn through your token budget or leak PII from your system prompt. Combine token-bucket rate limiting, prompt-injection detection, and sensitive-info filtering.

Create src/app/api/chat/route.ts.

// src/app/api/chat/route.ts
import arcjet, {
  detectBot,
  detectPromptInjection,
  sensitiveInfo,
  shield,
  tokenBucket,
} from "@arcjet/next";
import { NextResponse } from "next/server";
 
const aj = arcjet({
  key: process.env.ARCJET_KEY!,
  characteristics: ["userId"],
  rules: [
    shield({ mode: "LIVE" }),
    detectBot({ mode: "LIVE", allow: [] }),
    tokenBucket({
      mode: "LIVE",
      refillRate: 2_000,
      interval: "1h",
      capacity: 5_000,
    }),
    sensitiveInfo({
      mode: "LIVE",
      deny: ["CREDIT_CARD_NUMBER", "EMAIL", "PHONE_NUMBER"],
    }),
    detectPromptInjection({ mode: "LIVE", threshold: 0.5 }),
  ],
});
 
export async function POST(req: Request) {
  const { userId, message } = (await req.json()) as {
    userId: string;
    message: string;
  };
 
  const estimate = Math.ceil(message.length / 4);
 
  const decision = await aj.protect(req, {
    userId,
    requested: estimate,
    sensitiveInfoValue: message,
    detectPromptInjectionMessage: message,
  });
 
  if (decision.isDenied()) {
    if (decision.reason.isRateLimit()) {
      return NextResponse.json(
        { error: "AI usage limit exceeded" },
        { status: 429 },
      );
    }
    if (decision.reason.isSensitiveInfo()) {
      return NextResponse.json(
        { error: "Please remove personal info" },
        { status: 400 },
      );
    }
    if (decision.reason.isPromptInjection()) {
      return NextResponse.json(
        { error: "Prompt injection detected" },
        { status: 400 },
      );
    }
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }
 
  // Forward to your LLM provider of choice here.
  return NextResponse.json({ reply: "pong" });
}

The characteristics: ["userId"] line is critical. Without it, all users share one token bucket keyed by IP — a single power user depletes the quota for everyone. With a user-scoped characteristic, each account gets its own budget.

Step 8: Use the Arcjet Middleware for App-Wide Protection

Route-level protection is explicit but repetitive. For blanket coverage, put Arcjet in middleware.ts.

// middleware.ts
import arcjet, { createMiddleware, detectBot, shield } from "@arcjet/next";
 
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
 
const aj = arcjet({
  key: process.env.ARCJET_KEY!,
  rules: [
    shield({ mode: "LIVE" }),
    detectBot({
      mode: "LIVE",
      allow: ["CATEGORY:SEARCH_ENGINE", "CATEGORY:PREVIEW"],
    }),
  ],
});
 
export default createMiddleware(aj);

Keep the middleware rules minimal. Cost-sensitive features like token buckets belong on the expensive routes, not on every static asset fetch.

Step 9: Use DRY_RUN Mode When Tuning Rules

Never deploy a new rule in LIVE mode directly. Start in DRY_RUN so Arcjet logs the decision without blocking traffic.

fixedWindow({
  mode: "DRY_RUN",
  window: "1m",
  max: 20,
}),

Watch the Arcjet dashboard for a few days. If the decision stream shows only legitimate traffic hitting the limit, tighten the rule. Once the data looks right, flip to LIVE.

Step 10: Testing Your Implementation

Arcjet ships a testing helper so you do not need to hit real IPs during unit tests.

npm install -D @arcjet/test-utils

Write a spec that verifies the signup rule rejects disposable emails.

// __tests__/signup.test.ts
import { POST } from "@/app/api/signup/route";
 
test("blocks disposable email", async () => {
  const req = new Request("http://localhost/api/signup", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ email: "junk@mailinator.com" }),
  });
 
  const res = await POST(req);
  expect(res.status).toBe(400);
});

For local end-to-end testing of rate limits, run the for-loop from Step 4 with a short window (set window: "10s" temporarily) so you do not wait a full minute to see throttling kick in.

Step 11: Deploy to Production

Arcjet has no region lock-in. Deploy to Vercel, Netlify, Cloudflare Pages, Fly.io, or a self-hosted Node server — the SDK works the same everywhere.

On Vercel, add ARCJET_KEY as an environment variable under Project Settings and redeploy.

vercel env add ARCJET_KEY production
vercel --prod

In the Arcjet dashboard, open the Decisions view. You should see real traffic flowing and each rule's verdict counted per minute.

Step 12: Monitoring and Alerts

The dashboard surfaces three signals you want to watch:

  • Request volume by rule — a sudden spike in detectBot hits usually means a new scraper found your site.
  • Block rate — if more than 1% of real users are being blocked, your rules are too tight.
  • Spoofed bot ratio — a rising share means bad actors are rotating User-Agents to pose as Googlebot. Enable isSpoofedBot verification.

Wire the webhook integration to Slack or PagerDuty so you are alerted when the block rate crosses a threshold.

Troubleshooting

All requests are returning 403. Double-check that your bot allow list includes CATEGORY:SEARCH_ENGINE if you want Googlebot to reach you. During local development, your own browser should never be flagged — if it is, confirm you set mode: "DRY_RUN" on the detectBot rule until the allow list is right.

Rate limits do not reset. The fixed-window and token-bucket algorithms are keyed by IP by default. If you are testing from behind a corporate NAT, every developer shares the same key. Add characteristics: ["userId"] and pass a user ID to the protect() call to scope limits per account instead.

Shield flags legitimate search queries. Shield's OWASP detection can trip on form inputs that contain SQL-like words. Move Shield to DRY_RUN for that specific route, inspect the decisions tab, and add exemptions by moving the rule out of the global middleware.

Prompt injection detector misses jailbreaks. The threshold defaults to 0.5 on a 0–1 score. Lower it to 0.3 for stricter filtering, then monitor false-positive rates. For adversarial chat use cases, pair the detector with a system-prompt hardening layer — no single defense stops every attack.

Next Steps

Conclusion

In under an hour you added six layers of production-grade protection to a Next.js 15 app: a WAF, bot detection, fixed-window and token-bucket rate limits, email validation, PII filtering, and prompt-injection defense. All six live inside your codebase, all six are testable, and none of them require a reverse proxy or DNS change.

Security is not a one-time setup. Ship the rules in DRY_RUN, watch the dashboard, tune the thresholds, and promote to LIVE once the signal is clean. Then iterate — add a new rule every time you ship a new surface.


Want to read more tutorials? Check out our latest tutorial on Automating Workflows with Zapier and Webhooks in a Next.js App.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles