How to add app security in a few lines of code

Install an SDK, configure rules once, and call it at the top of each handler – no proxy and no DNS change.

7 min read
In short: You add security in a few lines of code by installing a security SDK, configuring a client with rules such as Shield WAF, bot detection, and rate limiting, and calling it at the top of each handler. The client returns a decision your code acts on. Start every rule in dry-run mode, review real traffic, then switch rules to live one at a time.

How do you add security to an app in a few lines of code?

Arcjet publishes this guide and sells the SDK it uses in the examples. The same steps apply to any in-application security library, and the final section covers when a few lines of code isn't the right approach.

You add security in a few lines of code by installing a security SDK, creating one client with the rules you want, and calling it at the top of each handler you need to protect. The client returns a decision, and your code allows the request, returns an error, or asks for more proof. There's no proxy to deploy and no DNS change, and the rules live in the same pull request as the feature they protect.

The pattern has four parts:

  1. Install the SDK for your framework and set an API key as a secret.
  2. Configure a client once, with rules such as a WAF, bot detection, and a rate limit.
  3. Call protect() in each route handler, or guard() in a tool call or background job.
  4. Act on the decision: continue, return a 403 or 429, or fall back to a safer path.

Start each rule in dry-run mode, which logs what the rule would have done without blocking anything. Switch it to live once you've reviewed a few days of real traffic.

What can a few lines of code protect against?

Each rule is one line in the client configuration. The following rules are the usual starting point for a web app or API:

RuleWhat it stopsWhere to use it first
Shield WAF

Common attacks such as SQL injection, cross-site scripting, and path traversal

Every route that reads user input
Bot detectionScrapers, credential stuffing, and scripted signupsSignup, login, pricing, and search
Rate limiting

Brute force, API abuse, and runaway cost, keyed to an IP address or a user ID

Login, password reset, expensive API routes, and AI endpoints
Email validationDisposable, invalid, and undeliverable email addressesSignup and invite forms
Sensitive-information detectionCard numbers, email addresses, and other PII in a request bodySupport forms and AI chat input
Prompt-injection detectionInstructions that try to override your model's system promptAI chat, retrieved documents, and tool results

A few lines of code won't fix an authorization bug, absorb a volumetric DDoS attack, or patch a vulnerable dependency. Keep your authorization checks, your hosting provider's DDoS protection, and your dependency scanner. The SDK adds the rules that need to know about the request and the user, which edge products and scanners can't see.

How do you add security to a Next.js route?

Install the SDK and add your key to .env.local. Don't prefix it with NEXT_PUBLIC_, which would ship it to the browser.

Terminal window
npm i @arcjet/next
Terminal window
ARCJET_KEY=ajkey_your_site_key

Create the client once, then call it at the top of the route handler:

import arcjet, { detectBot, shield, slidingWindow } from "@arcjet/next";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "DRY_RUN" }),
detectBot({ mode: "DRY_RUN", allow: ["CATEGORY:SEARCH_ENGINE"] }),
slidingWindow({ mode: "DRY_RUN", interval: 60, max: 100 }),
],
});
export async function POST(req: Request) {
const decision = await aj.protect(req);
if (decision.isDenied()) {
const status = decision.reason.isRateLimit() ? 429 : 403;
return new Response("Request blocked", { status });
}
// Your handler
return Response.json({ ok: true });
}

That's three rules and one check at the top of the handler. The rules run in dry-run mode, so every request still succeeds and each decision appears in the Arcjet dashboard. When the results look right, change "DRY_RUN" to "LIVE" one rule at a time.

How do you add security to a Python API?

The Python SDK supports FastAPI and Flask. Install it with pip install arcjet or uv add arcjet, then configure the client:

import os
from arcjet import Mode, arcjet, detect_bot, shield, token_bucket
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
aj = arcjet(
key=os.environ["ARCJET_KEY"],
rules=[
shield(mode=Mode.DRY_RUN),
detect_bot(mode=Mode.DRY_RUN, allow=[]),
token_bucket(mode=Mode.DRY_RUN, refill_rate=10, interval=60, capacity=20),
],
)
@app.post("/api/search")
async def search(request: Request):
decision = await aj.protect(request)
if decision.is_denied():
status = 429 if decision.reason_v2.type == "RATE_LIMIT" else 403
return JSONResponse({"error": "Request blocked"}, status_code=status)
return {"results": []}

An empty allow list blocks every automated client, including curl. If you need uptime monitors or link previews, add their categories to allow before you switch the rule to live.

How do you add security to a Go service?

Install the module with go get github.com/arcjet/arcjet-go, create one client, and reuse it across requests:

package main
import (
"log"
"net/http"
"os"
"time"
"github.com/arcjet/arcjet-go"
)
var aj, _ = arcjet.NewClient(arcjet.Config{
Key: os.Getenv("ARCJET_KEY"),
Rules: []arcjet.Rule{
arcjet.Shield(arcjet.ShieldOptions{Mode: arcjet.ModeDryRun}),
arcjet.DetectBot(arcjet.BotOptions{
Mode: arcjet.ModeDryRun,
Allow: []string{arcjet.BotCategorySearchEngine},
}),
arcjet.SlidingWindow(arcjet.SlidingWindowOptions{
Mode: arcjet.ModeDryRun,
Interval: time.Minute,
MaxRequests: 100,
}),
},
})
func handler(w http.ResponseWriter, r *http.Request) {
decision, err := aj.Protect(r.Context(), r)
if err != nil {
log.Printf("arcjet: %v", err)
} else if decision.IsDenied() {
status := http.StatusForbidden
if decision.Reason.IsRateLimit() {
status = http.StatusTooManyRequests
}
http.Error(w, "request blocked", status)
return
}
w.Write([]byte("ok"))
}

Interval is a time.Duration, so write time.Minute rather than 60, which Go reads as 60 nanoseconds. In production, check the error from NewClient instead of discarding it.

How do you protect code that has no HTTP request?

Tool calls, queue consumers, and MCP servers never receive a Request object, so protect() doesn't apply. Use guard() from @arcjet/guard instead. It takes the values you want to check directly:

import {
detectPromptInjection,
launchArcjet,
tokenBucket,
} from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const promptInjection = detectPromptInjection();
const lookups = tokenBucket({
bucket: "order-lookups",
refillRate: 30,
intervalSeconds: 60,
maxTokens: 30,
});
export async function lookupOrder(
args: { orderId: string; note: string },
session: { userId: string },
) {
const decision = await arcjet.guard({
label: "tools.lookup-order",
actor: session.userId,
rules: [
lookups({ key: session.userId, requested: 1 }),
promptInjection(args.note),
],
});
if (decision.conclusion === "DENY" || decision.hasFailedOpen()) {
return { error: "Lookup blocked" };
}
return orders.find({ id: args.orderId, userId: session.userId });
}

A direct guard() call fails open by default: if Arcjet can't be reached, it returns an allow with error codes. Checking hasFailedOpen() makes this tool fail closed instead, which suits actions that change data or cost money. For framework-specific helpers, see the LangChain, Vercel AI SDK, and Mastra guides.

How do you roll it out without breaking production?

  1. Deploy every rule in dry-run mode. Dry-run rules compute and record a decision but always return allow.
  2. Review a few days of decisions. Look for real users who would have been blocked – shared office IPs, monitoring tools, and partner integrations are the usual surprises.
  3. Switch one rule at a time to live. If support tickets rise, you know which rule caused it.
  4. Decide how each route handles errors. HTTP checks fail open by default, which suits most pages. For a route where an unchecked request is worse than a failed one, treat an errored decision as a deny.
  5. Keep the edge in front. Your CDN or cloud provider still handles volumetric DDoS; the SDK handles the rules that depend on your application.

What to check before you go live

  • The key is a secret. Store ARCJET_KEY in your platform's secret manager, and never in a variable that ships to the browser.
  • Client IPs are correct. Behind a load balancer or proxy, confirm the dashboard shows real public IP addresses, not your proxy's. For platform-specific setup, see securing Fly.io apps and detecting the client IP on Firebase.
  • Allowed bots are explicit. Search engines, uptime monitors, and link previews need to be on the allow list if you want them.
  • Rate-limit keys match your users. An IP-keyed limit punishes users behind a shared IP; key the limit to a user ID once people sign in.
  • The SDK version is pinned. Pin the major version so an upgrade is a deliberate change.

When is a few lines of code the wrong approach?

  • You can't change the application code. A WAF at the edge, or a runtime agent such as Aikido Zen that instruments the app, may be the only practical option.
  • Your stack isn't JavaScript, TypeScript, Python, or Go. Arcjet's SDKs cover those languages; other stacks need an edge WAF or an HTTP-based service.
  • You need to absorb large DDoS attacks. That belongs to your CDN or hosting provider.
  • The risk is a vulnerable dependency or insecure code. A code scanner finds those before deploy; a runtime SDK doesn't.

For a comparison of the three places you can enforce security, see SDK-based security vs WAF vs API gateway. For the full setup in your framework, see the Arcjet get started guide.

Frequently asked questions

How do you add security to an app in a few lines of code?

Install a security SDK for your framework, set its API key as a secret, create one client with rules such as Shield WAF, bot detection, and a rate limit, and call protect() at the top of each route handler. Act on the decision it returns: continue, or return a 403 or 429. Start in dry-run mode and switch rules to live after reviewing real traffic.

Do I need a proxy or a DNS change to add a WAF and bot protection?

Not with an in-application SDK. The rules run in your request handler, so you keep your current host and DNS. Keep your CDN or hosting provider in front for volumetric DDoS protection, which an SDK doesn't replace.

How do you protect tool calls and background jobs?

Code without an HTTP request, such as an agent tool, a queue consumer, or an MCP server, can't use protect(). Call guard() instead and pass the values to check directly, such as the user ID for a rate limit and the text to screen for prompt injection. Check hasFailedOpen() on actions that should fail closed.

How do you roll out new security rules safely?

Deploy every rule in dry-run mode, which records decisions without blocking. Review a few days of results for real users who would have been blocked, then switch one rule at a time to live so any change in support tickets has one cause.

When is a few lines of code the wrong approach?

When you can't change the application code, when your stack isn't JavaScript, TypeScript, Python, or Go, when you need to absorb large DDoS attacks, or when the risk is a vulnerable dependency that a code scanner should catch before deploy.

Application security in your code

Protect your application with Arcjet

Install the SDK, add one rule to the route that matters most, and leave it in dry run until the decisions look right.