Skip to content

Prompt injection detection for Next.js

Arcjet prompt injection detection evaluates each incoming prompt for injection patterns inside your application before it reaches the AI provider. Detected attacks are blocked before the AI call is made, protecting both your application behavior and your AI budget.

What is Arcjet? Arcjet is the runtime security platform that ships with your code. Enforce budgets, stop prompt injection, detect bots, and protect personal information with Arcjet's AI security building blocks.

In this example we use the Vercel AI SDK to create a simple AI chat endpoint with Next.js, and Arcjet to block prompt injection attacks before they reach the AI model. The same principles can be applied to any AI application, including those built with other frameworks.

We assume you already have a Next.js app set up.

Install the dependencies (prompt injection detection is available as of the Arcjet 1.3.0 JS SDK release):

Terminal window
# Export your Arcjet API key from https://console.arcjet.com
export ARCJET_KEY="ajkey_..."
npm install @arcjet/next ai @ai-sdk/openai

Create an AI chat endpoint:

/app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import arcjet, { detectPromptInjection, shield } from "@arcjet/next";
import type { UIMessage } from "ai";
import { convertToModelMessages, isTextUIPart, streamText } from "ai";
const aj = arcjet({
key: process.env.ARCJET_KEY!, // Get your site key from https://console.arcjet.com
rules: [
// Shield protects against common web attacks such as SQL injection
shield({ mode: "LIVE" }),
// Detect prompt injection attacks before they reach your AI model
detectPromptInjection({
mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
}),
],
});
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
// Check the most recent user message for prompt injection.
// Pass the full conversation if you want to scan all messages.
const lastMessage: string = (messages.at(-1)?.parts ?? [])
.filter(isTextUIPart)
.map((p) => p.text)
.join(" ");
const decision = await aj.protect(req, {
detectPromptInjectionMessage: lastMessage,
});
if (decision.isDenied()) {
if (decision.reason.isPromptInjection()) {
console.warn("Request blocked due to prompt injection");
return new Response("Prompt injection detected – rephrase your message", {
status: 403,
});
}
return new Response("Forbidden", { status: 403 });
}
// Arcjet approved – call your AI provider
const result = await streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}

And hook it up to a chat UI:

/app/page.tsx
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { messages, sendMessage } = useChat({
onError: async (e) => setErrorMessage(e.message),
});
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((message) => (
<div key={message.id} className="whitespace-pre-wrap">
{message.role === "user" ? "User: " : "AI: "}
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>;
}
})}
</div>
))}
{errorMessage && (
<div className="text-red-500 text-sm mb-4">{errorMessage}</div>
)}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
setErrorMessage(null);
}}
>
<input
className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}

Then run the server:

Terminal window
npm run dev

Requests appear in your Arcjet dashboard in real time.

Need help with anything? Email support@arcjet.com to get support from our engineering team.

Discussion