next-armor
v0.1.0
Published
Drop-in security for Next.js API routes. Rate limiting, bot detection, threat scoring, input validation, SSRF protection. Zero cloud dependencies.
Maintainers
Readme
next-armor
Drop-in security for Next.js API routes. One function. Zero cloud dependencies.
npm install next-armorThe Problem
Every Next.js API route needs rate limiting, input validation, bot protection. Most developers skip it because there's no frictionless option — existing solutions require cloud services, Redis, or stitching together 5 packages.
The Solution
import { armor } from "next-armor";
import { z } from "zod";
export const POST = armor(
{
rateLimit: { max: 10, window: "1m" },
validate: z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
}),
honeypot: true,
},
async (req, { data, ip }) => {
// data is validated, sanitized, bot-checked, rate-limited
console.log(data.email); // typed, safe
return Response.json({ success: true });
},
);That's it. Rate limiting, input validation, bot detection, XSS sanitization — all handled before your code runs.
What You Get
| Feature | What It Does | | -------------------- | ------------------------------------------------------------------ | | Rate limiting | In-memory token bucket. No Redis required. | | Input validation | Zod schema validation with typed output. | | Bot detection | Invisible honeypot fields. No CAPTCHA. | | Threat scoring | Behavioral analysis — burst detection, path probing, malicious UA. | | XSS sanitization | Auto-escapes HTML in validated strings. | | SSRF protection | Blocks private IPs and cloud metadata endpoints. | | IP extraction | Works with Vercel, Cloudflare, standard proxies. |
How It Compares
| | next-armor | Arcjet | DIY | | ---------------------- | ---------- | ------------- | ------------------- | | Cloud service required | No | Yes ($23/mo+) | No | | Redis required | No | No | Usually | | Setup | 1 function | SDK + API key | 50+ lines per route | | Threat scoring | Built-in | Built-in | Build it yourself | | Honeypot bot detection | Built-in | No | Build it yourself | | SSRF protection | Built-in | No | Build it yourself | | Price | Free | $23/mo+ | Your time |
Usage
Quick Start — One Line Protection
// app/api/contact/route.ts
import { armor } from "next-armor";
import { z } from "zod";
export const POST = armor(
{
rateLimit: { max: 5, window: "1m" },
validate: z.object({
email: z.string().email(),
message: z.string().min(1).max(5000),
}),
honeypot: true,
},
async (req, { data }) => {
await sendEmail(data.email, data.message);
return Response.json({ sent: true });
},
);Rate Limiting Only
export const GET = armor(
{
rateLimit: { max: 30, window: "1m" },
},
async (req) => {
return Response.json({ data: await fetchData() });
},
);Full Threat Scoring
export const POST = armor(
{
rateLimit: { max: 10, window: "1m" },
validate: schema,
honeypot: true,
threatScore: true,
blockAt: ThreatLevel.SUSPICIOUS, // Block earlier than default
},
async (req, { data, ip, threat }) => {
if (threat?.level === ThreatLevel.CAUTIOUS) {
// Log but allow
console.log(`Cautious request from ${ip}`);
}
return Response.json({ success: true });
},
);Honeypot Fields (Client Side)
Add hidden fields to your forms that bots auto-fill:
// In your form component
<form action="/api/contact" method="POST">
{/* Hidden from real users, visible to bots */}
<div
aria-hidden="true"
style={{
position: "absolute",
left: "-9999px",
height: 0,
width: 0,
overflow: "hidden",
}}
>
<input type="text" name="website_url" tabIndex={-1} autoComplete="off" />
<input type="text" name="phone_number" tabIndex={-1} autoComplete="off" />
</div>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>When honeypot: true is set in armor(), it automatically checks these fields and silently rejects bot submissions with a fake 200 response (so bots don't learn they were caught).
SSRF Protection
Validate URLs before server-side fetching:
import { validateFetchUrl } from "next-armor";
const result = await validateFetchUrl(userProvidedUrl);
if (!result.valid) {
return Response.json({ error: result.reason }, { status: 400 });
}
// Safe to fetch
const data = await fetch(userProvidedUrl);Blocks: private IPs (127.x, 10.x, 192.168.x), cloud metadata (169.254.169.254), DNS rebinding attacks, non-HTTPS protocols.
Using Individual Modules
Don't want the wrapper? Use modules directly:
import {
rateLimit,
getClientIp,
checkHoneypot,
evaluateRequest,
escapeHtml,
validateFetchUrl,
ThreatLevel,
} from "next-armor";
// Build your own flow
const limiter = rateLimit({ interval: 60_000, limit: 10 });
export async function POST(request: Request) {
const ip = getClientIp(request);
const { success } = await limiter.check(ip);
if (!success) {
/* handle */
}
const assessment = evaluateRequest(
ip,
"/api/submit",
request.headers.get("user-agent") || "",
);
if (assessment.level === ThreatLevel.BLOCKED) {
/* handle */
}
// ... your logic
}Configure Threat Scoring
Customize detection rules at app startup:
import { configureThreatScore } from "next-armor";
configureThreatScore({
// Add custom probe paths to detect
probePaths: ["/api/admin", "/api/internal"],
// Adjust thresholds
thresholds: { cautious: 2, suspicious: 5, blocked: 8 },
// Custom tracking window
windowMs: 5 * 60 * 1000, // 5 minutes
});Window Formats
Rate limit windows accept human-readable strings:
| Format | Duration |
| ------- | ----------------- |
| '30s' | 30 seconds |
| '1m' | 1 minute |
| '5m' | 5 minutes |
| '1h' | 1 hour |
| 60000 | 60,000ms (number) |
How Threat Scoring Works
The threat engine evaluates every request using red (threat) and blue (trust) signals:
Red signals (increase score):
- Burst requests (8+ in 10 seconds)
- Path probing (wp-admin, .env, .git, etc.)
- Malicious user agents (sqlmap, nikto, curl, etc.)
- Honeypot triggers
- Failed auth attempts
Blue signals (decrease score):
- Normal browsing pattern (2-10 req/min, no red flags)
- Legitimate browser user agent
- Clean history (60+ seconds, zero incidents)
Threat levels:
- TRUSTED (0-2): Normal traffic
- CAUTIOUS (3-5): Slightly suspicious, monitor
- SUSPICIOUS (6-8): Likely malicious, consider blocking
- BLOCKED (9+): Block the request
Requirements
- Node.js 18+
- Next.js 13+ (App Router)
zod(optional — only needed if usingvalidate)
FAQ
Does it work on Vercel Edge?
Yes. Everything uses standard Web APIs (Request/Response). No Node.js-specific APIs except validateFetchUrl which uses dns/promises.
What happens on cold starts?
Rate limit and threat score data resets. This is acceptable for most deployments. If you need persistent rate limiting, use Upstash Redis behind the same rateLimit() interface.
Can I use it without the wrapper?
Yes. Every module is independently importable. Use armor() for convenience or individual functions for control.
Is Zod required?
Only if you use the validate option. It's a peer dependency marked as optional.
License
MIT
Author
Trent Jackson — Building Pureformance, an architecture practice for founders whose systems are straining.
