Next.js has three main surfaces that attract bot abuse: API routes, Server Actions, and edge middleware. Each has different constraints for adding bot detection, and each needs a slightly different integration approach. This guide covers all three.
Why Next.js Apps Are Common Bot Targets
Server Actions in particular are attractive to bots because they're easy to call directly with a POST request once the action ID is known, bypassing any client-side UI entirely. Form endpoints, waitlist signups, and checkout flows built with Server Actions routinely see credential-stuffing and scraping traffic that never touches the rendered page.
Option 1: Edge Middleware (Broadest Coverage)
Middleware runs before any route handler, making it the right place for blanket protection on high-risk paths:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const res = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DEVICE_AI_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
ip: req.headers.get('x-forwarded-for'),
userAgent: req.headers.get('user-agent'),
}),
});
const { botScore } = await res.json();
if (botScore > 0.85) {
return new NextResponse('Forbidden', { status: 403 });
}
return NextResponse.next();
}
export const config = { matcher: ['/api/:path*', '/checkout/:path*'] };
Option 2: API Route Handlers
For finer-grained control (e.g., different thresholds per endpoint), call device.ai inside the route handler itself:
// app/api/signup/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const verifyRes = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.DEVICE_AI_KEY}` },
body: JSON.stringify({ ip: req.headers.get('x-forwarded-for') }),
});
const { botScore } = await verifyRes.json();
if (botScore > 0.8) {
return NextResponse.json({ error: 'Request blocked' }, { status: 403 });
}
// proceed with signup logic
}
Option 3: Server Actions
Server Actions don't have direct access to request headers by default in all cases, so pass along what you can capture client-side (or use headers() from next/headers where available) and verify before mutating state:
// app/actions.ts
'use server';
import { headers } from 'next/headers';
export async function submitWaitlist(formData: FormData) {
const hdrs = headers();
const verifyRes = await fetch('https://device.ai/v1/verify', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.DEVICE_AI_KEY}` },
body: JSON.stringify({ ip: hdrs.get('x-forwarded-for'), userAgent: hdrs.get('user-agent') }),
});
const { botScore } = await verifyRes.json();
if (botScore > 0.85) throw new Error('Request blocked');
// proceed with mutation
}
Choosing Thresholds
Start conservative (block only botScore > 0.9) on customer-facing forms to avoid false positives, and tighten on endpoints with a history of abuse (waitlists, coupon codes, referral programs) where the cost of a false positive is low.
Get Started
Get a free API key at device.ai — no signup required — and add verification to your first route in minutes.