Add Arcjet Protection to a Route
Add runtime security to a route handler using Arcjet. This skill guides you through setting up the CLI, detecting the framework, configuring rules, and verifying protection.
Reference
Read https://docs.arcjet.com/llms.txt for comprehensive SDK documentation covering all frameworks, rule types, and configuration options.
Step 0: Set Up the Arcjet CLI
The Arcjet CLI is the primary tool for authenticating, managing sites, configuring remote rules, and verifying protection. Install it if not already available:
# Via bunx (no install required)
bunx @arcjet/cli --help
# Or install globally via bun
bun add -g @arcjet/cli
# Or via Homebrew
brew install arcjet
Authenticate
arcjet auth login
Opens the browser for authentication. Check status with arcjet auth status.
Site & Key Setup
# List your teams
arcjet teams list
# List sites for a team
arcjet sites list --team-id <team-id>
# Create a new site
arcjet sites create --team-id <team-id> --name "My App" --confirm
# Get the SDK key for a site
arcjet sites get-key --site-id <site-id>
Add the key to your environment file (.env.local for Next.js/Astro, .env for others) as ARCJET_KEY.
Step 1: Detect the Framework
Check the project for framework indicators:
package.jsondependencies:next,express,fastify,@nestjs/core,@sveltejs/kit,hono,@remix-run/node,react-router,astro,nuxtbun.lockborbun.lock→ Bun runtimedeno.json→ Deno runtimepyproject.tomlorrequirements.txtwithfastapiorflask→ Python
Select the correct Arcjet adapter package:
| Framework | Package |
|---|---|
| Next.js | @arcjet/next |
| Express / Node.js / Hono | @arcjet/node |
| Fastify | @arcjet/fastify |
| NestJS | @arcjet/nest |
| SvelteKit | @arcjet/sveltekit |
| Remix | @arcjet/remix |
| React Router | @arcjet/react-router |
| Astro | @arcjet/astro |
| Bun | @arcjet/bun |
| Deno | npm:@arcjet/deno |
| Python (FastAPI/Flask) | arcjet (pip) |
Step 2: Check for Existing Arcjet Setup
Search the project for an existing shared Arcjet client file (commonly lib/arcjet.ts, src/lib/arcjet.ts, lib/arcjet.py, or similar).
If no client exists:
- Install the correct adapter package.
- Check if
ARCJET_KEYis set in the environment file (.env.localfor Next.js/Astro,.envfor others). If not, obtain the key in this priority order:- CLI (preferred): Run
arcjet sites get-key --site-id <site-id>(requiresarcjet auth loginfirst — see Step 0) - MCP: If the Arcjet MCP server is connected, use
list-teams→list-sites→get-site-key - Manual (last resort): Add a placeholder and tell the user to get a key from https://app.arcjet.com
- Also add
ARCJET_ENV=developmentto the env file
- CLI (preferred): Run
- Create a shared client file with
shield()as the base rule. This file should export the Arcjet instance for reuse across routes withwithRule().
If a client already exists: Import it. Do not create a new instance.
Step 3: Choose Protection Rules
Select rules based on the route's purpose. If the user specified what they want, use that. Otherwise, infer from context:
| Route type | Recommended rules |
|---|---|
| Public API endpoint | shield() + detectBot() + slidingWindow() (use fixedWindow() only if hard per-window caps are needed) |
| Form handler / signup | shield() + validateEmail() + slidingWindow() |
| Authentication endpoint | shield() + slidingWindow() (strict, low limits) |
| AI / LLM endpoint | shield() + detectBot() + tokenBucket() + content filtering |
| Webhook receiver | shield() + filter rules for allowed IPs |
| General server route | shield() + detectBot() |
For routes that need to detect sophisticated bots (headless browsers, advanced scrapers) — especially form submissions, login/signup pages, and other abuse-prone endpoints — recommend adding Arcjet advanced signals. This is a browser-based detection system using client-side telemetry that complements server-side detectBot() rules. See https://docs.arcjet.com/bot-protection/advanced-signals for setup instructions.
Apply route-specific rules using withRule() on the shared instance — do not modify the shared instance directly.
Step 4: Add Protection to the Handler
Call protect() inside the route handler (not in middleware), only once per request, passing the framework's request object directly. For Next.js pages/server components: use import { request } from "@arcjet/next" then const req = await request().
Use this pattern:
const decision = await aj.protect(req);
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return Response.json(
{ error: "Too many requests" },
{ status: 429 },
);
}
if (decision.reason.isBot() || decision.reason.isShield() || decision.reason.isFilterRule()) {
return Response.json(
{ error: "Forbidden" },
{ status: 403 },
);
}
if (decision.reason.isSensitiveInfo()) {
return Response.json(
{ error: "Bad request" },
{ status: 400 },
);
}
}
// Arcjet fails open — log errors but allow the request
if (decision.isErrored()) {
console.warn("Arcjet error:", decision.reason.message);
}
// Proceed with route handler logic...
Adapt the response format to your framework (e.g., res.status(429).json(...) for Express, JsonResponse for Django).
Step 5: Verify with the CLI
After adding protection, use the CLI to verify decisions are firing correctly. This creates a feedback loop: start the app, hit the route, inspect decisions, adjust if needed.
1. Start Watching
In a separate terminal, stream live request decisions:
arcjet watch --site-id <site-id>
This polls for new decisions and prints them as they arrive. Use --conclusion DENY to filter to denials only, or --interval 2 for faster polling.
2. Hit the Protected Route
Start the app and send requests to the protected route. Each request should produce a decision visible in the watch output.
3. Inspect Decisions
If a decision doesn't match expectations:
# List recent requests (filter to denials)
arcjet requests list --site-id <site-id> --conclusion DENY --limit 10
# Get full details for a specific request
arcjet requests details --site-id <site-id> --request-id <request-id>
# Plain-English explanation of why a request was allowed/denied
arcjet requests explain --site-id <site-id> --request-id <request-id>
4. Adjust and Repeat
If rules aren't firing as expected, adjust the code and re-test. Use arcjet watch to confirm each change produces the expected decisions.
The Arcjet dashboard at https://app.arcjet.com is also available for visual inspection.
Step 6: Manage Remote Rules via CLI (Optional)
Remote rules apply globally to all requests for a site and can be managed without code changes or redeployment. Supported types: rate_limit, bot, shield, filter.
# Create a rule (always starts in DRY_RUN)
arcjet rules create --site-id <site-id> --type rate_limit --max 100 --window 60 --confirm
arcjet rules create --site-id <site-id> --type shield --confirm
arcjet rules create --site-id <site-id> --type bot --deny CATEGORY:SEARCH_ENGINE --confirm
# Check what a dry-run rule would block
arcjet analyze dry-run-impact --site-id <site-id>
# Promote to LIVE once verified
arcjet rules promote --site-id <site-id> --rule-id <rule-id> --confirm
# List / update / delete rules
arcjet rules list --site-id <site-id>
arcjet rules update --site-id <site-id> --rule-id <rule-id> --max 200 --confirm
arcjet rules delete --site-id <site-id> --rule-id <rule-id> --confirm
Step 7: Traffic Analysis
Use the CLI to monitor traffic patterns and investigate issues:
# Full security briefing (traffic, denials, quota, active rules)
arcjet briefing --site-id <site-id>
# Traffic analysis over 14 days
arcjet analyze traffic --site-id <site-id> --days 14
# Detect anomalies (spikes, geographic shifts, new threats)
arcjet analyze anomalies --site-id <site-id>
# Investigate a specific IP
arcjet analyze ip --site-id <site-id> --ip 1.2.3.4
Common Mistakes to Avoid
- Creating a new Arcjet instance per request (causes connection overhead)
- Using Arcjet in Next.js middleware (fires on every request, no route context)
- Calling
protect()multiple times in one request (double-counts rate limits) - Hardcoding
ARCJET_KEYinstead of using environment variables - Using
app.use()as Express middleware instead of per-route protection
CLI Quick Reference
| Task | Command |
|---|---|
| Install/run CLI | bunx @arcjet/cli or brew install arcjet |
| Authenticate | arcjet auth login |
| Check auth status | arcjet auth status |
| List teams | arcjet teams list |
| List sites | arcjet sites list --team-id <id> |
| Create site | arcjet sites create --team-id <id> --name "Name" --confirm |
| Get SDK key | arcjet sites get-key --site-id <id> |
| Watch live requests | arcjet watch --site-id <id> |
| List requests | arcjet requests list --site-id <id> |
| Explain a decision | arcjet requests explain --site-id <id> --request-id <id> |
| Create rule (DRY_RUN) | arcjet rules create --site-id <id> --type <type> ... |
| List rules | arcjet rules list --site-id <id> |
| Promote to LIVE | arcjet rules promote --site-id <id> --rule-id <id> --confirm |
| Security briefing | arcjet briefing --site-id <id> |
| Analyze traffic | arcjet analyze traffic --site-id <id> |
Global Flags
All commands support:
--output text|json— output format (default: text on TTY, json otherwise)--fields <list>— comma-separated fields to include in JSON output--no-color— disable ANSI colors (also honorsNO_COLORenv var)--timeout <duration>— max execution time (e.g.30s,5m; 0 disables)
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error (unknown command, API failure, network error) |
| 2 | Authentication error (not logged in, token expired) |
| 3 | Input validation error (invalid ID, value out of range) |
| 4 | Confirmation required (mutation needs --confirm) |