Why Next.js Apps Need Bot Detection
Next.js applications have become prime targets for bot attacks. Your API routes handle form submissions, checkout flows, and user-generated content. Server Actions make it trivial for bots to trigger backend logic without any frontend interaction. Middleware runs at the edge, but without bot verification, malicious requests still consume your resources.
Credential-stuffing attacks test login forms at scale. Scrapers abuse your API routes. Spammers post to user-generated content endpoints. Each of these attacks succeeds because your Next.js app can't distinguish between legitimate users and automated bots.
This guide shows you how to add real bot detection to Next.js using device.ai—with working code for App Router, Pages Router, middleware, Server Actions, and edge runtime. No complex setup. No OAuth. Just instant API keys and straightforward integration.
What You'll Need
- A Next.js project (App Router or Pages Router)
- A free device.ai API key (1,000 daily verifications): visit device.ai
- Five minutes to integrate
How device.ai Bot Detection Works
device.ai returns a single score (0.0 to 1.0) representing how likely a request is from a bot:
- 0.0-0.3: High confidence bot. Block immediately.
- 0.3-0.6: Uncertain. Challenge with CAPTCHA or require email verification.
- 0.6-1.0: Likely human. Allow.
You control the thresholds. You decide what to do. device.ai just gives you the score.
Step 1: Get Your API Key (1 minute)
Visit device.ai, click "Get Free API Key", and copy your key. No signup. No credit card. Your first 1,000 daily verifications are free.
Add it to your .env.local:
DEVICE_AI_API_KEY=dv_live_your_key_here
Pattern 1: Protect API Routes (App Router)
Create a helper function to verify bot status:
// lib/device-ai.ts
export async function verifyDevice(req: Request) {
const userAgent = req.headers.get('user-agent') || '';
const ip = req.headers.get('x-forwarded-for') || 'unknown';
try {
const response = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: process.env.DEVICE_AI_API_KEY,
userAgent,
ip,
}),
});
if (!response.ok) {
// If device.ai is down, fail open (allow request)
console.error('device.ai verification failed:', response.status);
return { score: 0.8, bot: false, risk: 'unknown' };
}
return response.json();
} catch (error) {
console.error('device.ai network error:', error);
return { score: 0.8, bot: false, risk: 'unknown' };
}
}
Now use it in an API route:
// app/api/submit-form/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyDevice } from '@/lib/device-ai';
export async function POST(req: NextRequest) {
const verification = await verifyDevice(req);
if (verification.score < 0.3) {
return NextResponse.json(
{ error: 'Bot detected. Please try again.' },
{ status: 403 }
);
}
if (verification.score < 0.6) {
return NextResponse.json(
{ error: 'Verification required. Please refresh and try again.' },
{ status: 429 }
);
}
const body = await req.json();
// Your form processing logic here
return NextResponse.json({ success: true });
}
Pattern 2: Protect Server Actions (App Router)
Server Actions are especially vulnerable to bot abuse. Here's how to protect them:
// app/actions.ts
'use server';
import { verifyDevice } from '@/lib/device-ai';
import { headers } from 'next/headers';
export async function submitSignup(formData: FormData) {
const headersList = await headers();
const userAgent = headersList.get('user-agent') || '';
const ip = headersList.get('x-forwarded-for') || 'unknown';
try {
const response = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: process.env.DEVICE_AI_API_KEY,
userAgent,
ip,
}),
});
const verification = await response.json();
if (verification.score < 0.3) {
throw new Error('Bot detected');
}
if (verification.score < 0.6) {
throw new Error('Please verify and try again');
}
} catch (error) {
console.error('Bot verification failed:', error);
}
const email = formData.get('email');
// Your signup logic here
return { success: true, email };
}
Pattern 3: Middleware (Broadest Coverage, App Router)
Middleware runs before routes are processed, giving you a single place to block bots:
// middleware.ts (root of project)
import { NextRequest, NextResponse } from 'next/server';
export const config = {
matcher: [
'/api/submit-form',
'/api/checkout',
'/api/user-content',
'/api/feedback',
],
};
export async function middleware(req: NextRequest) {
const userAgent = req.headers.get('user-agent') || '';
const ip = req.headers.get('x-forwarded-for') || req.ip || 'unknown';
try {
const response = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: process.env.DEVICE_AI_API_KEY,
userAgent,
ip,
}),
signal: AbortSignal.timeout(2000),
});
if (!response.ok) {
console.warn('device.ai unavailable:', response.status);
return NextResponse.next();
}
const verification = await response.json();
if (verification.score < 0.2) {
return NextResponse.json(
{ error: 'Access denied' },
{ status: 403 }
);
}
return NextResponse.next();
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.warn('device.ai verification timed out');
} else {
console.error('device.ai middleware error:', error);
}
return NextResponse.next();
}
}
Pattern 4: Pages Router (Legacy)
If you're using Next.js Pages Router, the pattern is similar:
// pages/api/submit.ts
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const userAgent = req.headers['user-agent'] || '';
const ip = (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || 'unknown';
try {
const response = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: process.env.DEVICE_AI_API_KEY,
userAgent,
ip,
}),
});
const verification = await response.json();
if (verification.score < 0.3) {
return res.status(403).json({ error: 'Bot detected' });
}
return res.status(200).json({ success: true });
} catch (error) {
console.error('Bot verification failed:', error);
return res.status(200).json({ success: true });
}
}
Handling Rate Limits and Failures
What if device.ai returns 429 (rate limit)?
Your free tier gives 1,000 verifications per day. For most applications, this is plenty. If you hit the limit:
const response = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
key: process.env.DEVICE_AI_API_KEY,
userAgent,
ip,
}),
});
if (response.status === 429) {
// Rate limit exceeded—upgrade or cache results
console.warn('device.ai rate limit exceeded');
return { score: 0.8, bot: false, risk: 'unknown' };
}
Caching to reduce API calls:
const verificationCache = new Map();
function getCacheKey(userAgent, ip) {
return ip + '-' + userAgent.slice(0, 50);
}
export async function verifyDeviceCached(req, cacheTtlMs = 3600000) {
const userAgent = req.headers.get('user-agent') || '';
const ip = req.headers.get('x-forwarded-for') || 'unknown';
const cacheKey = getCacheKey(userAgent, ip);
const cached = verificationCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < cacheTtlMs) {
return cached.score;
}
const verification = await verifyDevice(req);
verificationCache.set(cacheKey, {
score: verification.score,
timestamp: Date.now(),
});
return verification.score;
}
Next.js Edge Runtime Considerations
If you're using Vercel and edge middleware or edge runtime API routes:
- Fetch works: You can call device.ai from edge functions
- Keep timeouts short: Use AbortSignal.timeout(2000) for bot verification
- No synchronous storage: Use Vercel KV or similar for caching
- Fail open: Allow requests on timeout rather than reject
Testing Your Bot Detection
Manually test your integration:
curl -X POST https://device.ai/v1/verify \
-H "Content-Type: application/json" \
-d '{
"key": "dv_live_YOUR_KEY",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit",
"ip": "192.168.1.1"
}'
For automated testing, mock the fetch call:
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
score: 0.85,
bot: false,
risk: 'low',
}),
})
);
Monitoring and Observability
Add logging to understand bot patterns:
interface BotEvent {
timestamp: Date;
ip: string;
score: number;
risk: string;
path: string;
action: 'blocked' | 'challenged' | 'allowed';
}
const botEvents = [];
export async function logBotEvent(req, verification, action) {
const event = {
timestamp: new Date(),
ip: req.headers.get('x-forwarded-for') || 'unknown',
score: verification.score,
risk: verification.risk,
path: new URL(req.url).pathname,
action,
};
botEvents.push(event);
// Send to analytics
}
Common Pitfalls and Solutions
| Pitfall | Solution |
|---|---|
| Timeout in middleware blocks legitimate requests | Use AbortSignal.timeout(2000) and fail open (allow) on timeout |
| Missing x-forwarded-for header reveals incorrect IP | Use req.ip as fallback in App Router, req.socket.remoteAddress in Pages Router |
| Rate limit exceeded on free tier | Cache results or upgrade. See device.ai/upgrade |
| False positives block legitimate VPN users | Adjust threshold from 0.3 to 0.4 or lower |
| Server Actions don't have request headers context | Use headers() helper from next/headers |
What's Next?
You've now protected your Next.js app from basic bot attacks. As your app grows:
- Monitor bot trends: Log and analyze detection events to understand attack patterns
- Tune thresholds: Adjust your 0.3/0.6 cutoffs based on false positive feedback
- Implement caching: Cache verification results to reduce API calls and costs
- Scale to paid plan: When you exceed 1,000 daily verifications, upgrade at device.ai/upgrade
Get Started Now
Don't let bots abuse your Next.js app. Get your free device.ai API key at device.ai. No signup. No credit card. Integrate in 5 minutes. Protect your API routes, Server Actions, and forms immediately.
Questions? Check the device.ai docs or the examples above. Happy protecting!