Prompt Injection Detection with TypeScript
Integrate prompt injection detection in TypeScript and Next.js — server-side screening, API calls, error handling, and where to place guards in your request flow.
Integrate prompt injection detection in TypeScript by calling IdenticAPI's Prompt Injection Shield from server-side code — before user messages, retrieved chunks, or agent fetch results reach your LLM. Client-side-only checks are bypassable; place guards in Next.js Route Handlers, Server Actions, or Node.js middleware that runs on every untrusted input path.
This guide covers types, fetch wrappers, error handling, verdict policies, and placement in a Next.js App Router chat flow.
API contract
POST /api/v1/security/prompt-injection
Authorization: Bearer idapi_test_your_key_here
Content-Type: application/json
{"text": "string (1–32,000 chars)", "source": "optional", "context": "optional"}
Response fields:
| Field | Type | Description |
|---|---|---|
request_id | string | Correlation ID for logs |
api | string | "prompt-injection-shield" |
verdict | "safe" | "suspicious" | "unsafe" | Primary decision input |
risk | string | "low", "medium", "high" |
findings | array | { category, reason, confidence?, start?, end? } |
reasons | string[] | Summary strings |
usage_units | number | Billing meter |
Full reference: Prompt Injection Shield documentation. Product overview: Prompt Injection Shield.
Types and client module
Create lib/identicapi/prompt-injection.ts:
export type InjectionVerdict = "safe" | "suspicious" | "unsafe";
export type InjectionFinding = {
category: string;
reason: string;
confidence?: number;
start?: number;
end?: number;
};
export type PromptInjectionResponse = {
request_id: string;
api: string;
verdict: InjectionVerdict;
risk: string;
findings: InjectionFinding[];
reasons: string[];
usage_units: number;
processing_time_ms?: number;
detector_version?: string;
};
export class PromptInjectionApiError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly code?: string
) {
super(message);
this.name = "PromptInjectionApiError";
}
}
export type ScreenPromptOptions = {
source?: string;
context?: string;
baseUrl?: string;
apiKey?: string;
signal?: AbortSignal;
};
const DEFAULT_BASE_URL =
process.env.IDENTICAPI_BASE_URL ?? "https://www.identicapi.com";
export async function screenPromptInjection(
text: string,
options: ScreenPromptOptions = {}
): Promise<PromptInjectionResponse> {
const apiKey = options.apiKey ?? process.env.IDENTICAPI_API_KEY;
if (!apiKey) {
throw new PromptInjectionApiError(
"IDENTICAPI_API_KEY is not configured",
500,
"missing_api_key"
);
}
if (!text || text.length > 32_000) {
throw new PromptInjectionApiError(
"text must be between 1 and 32,000 characters",
400,
"invalid_text_length"
);
}
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
const url = `${baseUrl}/api/v1/security/prompt-injection`;
let response: Response;
try {
response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
text,
...(options.source ? { source: options.source } : {}),
...(options.context ? { context: options.context } : {})
}),
signal: options.signal
});
} catch (err) {
const message = err instanceof Error ? err.message : "Network request failed";
throw new PromptInjectionApiError(message, 503, "network_error");
}
const body = (await response.json().catch(() => null)) as
| (PromptInjectionResponse & { error?: { code?: string; message?: string } })
| null;
if (!response.ok || !body) {
throw new PromptInjectionApiError(
body?.error?.message ?? `Prompt injection API returned ${response.status}`,
response.status,
body?.error?.code
);
}
return body;
}
Verdict policy helper
Map API verdicts to application actions:
export type GuardAction = "allow" | "review" | "block";
export function verdictToAction(verdict: InjectionVerdict): GuardAction {
switch (verdict) {
case "unsafe":
return "block";
case "suspicious":
return "review";
default:
return "allow";
}
}
Adjust suspicious handling per product: high-risk flows should treat review as block.
Next.js Route Handler example
app/api/chat/route.ts:
import { NextRequest, NextResponse } from "next/server";
import {
screenPromptInjection,
verdictToAction,
PromptInjectionApiError
} from "@/lib/identicapi/prompt-injection";
export async function POST(request: NextRequest) {
let message: string;
try {
const body = await request.json();
message = typeof body.message === "string" ? body.message.trim() : "";
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
if (!message) {
return NextResponse.json({ error: "message is required" }, { status: 400 });
}
let screen;
try {
screen = await screenPromptInjection(message, {
source: "chat_input",
context: "route=/api/chat"
});
} catch (err) {
if (err instanceof PromptInjectionApiError) {
// Fail closed for security-sensitive deployments; fail open only with explicit product decision
console.error("injection_screen_failed", {
status: err.status,
code: err.code,
message: err.message
});
return NextResponse.json(
{ error: "Security screening temporarily unavailable" },
{ status: 503 }
);
}
throw err;
}
const action = verdictToAction(screen.verdict);
console.info("prompt_injection_screen", {
request_id: screen.request_id,
verdict: screen.verdict,
risk: screen.risk,
categories: screen.findings.map((f) => f.category),
action
});
if (action === "block") {
return NextResponse.json(
{
error: "Message blocked by security policy",
request_id: screen.request_id
},
{ status: 400 }
);
}
if (action === "review") {
// Queue for human review or require rephrase — do not forward to LLM in strict mode
return NextResponse.json(
{
error: "Message flagged for review",
request_id: screen.request_id
},
{ status: 422 }
);
}
// action === "allow" — proceed to your LLM provider call
const llmReply = await callYourLlmProvider(message);
return NextResponse.json({ reply: llmReply, request_id: screen.request_id });
}
async function callYourLlmProvider(message: string): Promise<string> {
// Replace with your provider integration
return `Echo: ${message.slice(0, 100)}`;
}
Screening RAG chunks
Loop retrieved chunks before prompt assembly:
export async function filterSafeChunks(
chunks: { id: string; text: string }[]
): Promise<{ id: string; text: string }[]> {
const safe: { id: string; text: string }[] = [];
for (const chunk of chunks) {
const result = await screenPromptInjection(chunk.text, {
source: "rag_chunk",
context: `chunk_id=${chunk.id}`
});
if (verdictToAction(result.verdict) === "allow") {
safe.push(chunk);
continue;
}
console.warn("rag_chunk_blocked", {
chunk_id: chunk.id,
request_id: result.request_id,
verdict: result.verdict,
reasons: result.reasons
});
}
return safe;
}
Parallelize with Promise.all only if rate limits allow — sequential is safer for quota control.
Environment variables
IDENTICAPI_API_KEY=idapi_test_your_key_here
IDENTICAPI_BASE_URL=https://www.identicapi.com # optional override
Never expose API keys in client bundles (NEXT_PUBLIC_*).
Unit testing with mocks
import { describe, it, expect, vi } from "vitest";
import { verdictToAction } from "@/lib/identicapi/prompt-injection";
describe("verdictToAction", () => {
it("blocks unsafe", () => {
expect(verdictToAction("unsafe")).toBe("block");
});
it("reviews suspicious", () => {
expect(verdictToAction("suspicious")).toBe("review");
});
});
Add fixture integration tests per Prompt Injection Testing.
Where to place guards
| Location | Screen |
|---|---|
| Chat Route Handler | User message |
| Document upload API | Extracted full text |
| RAG service | Each chunk at retrieve |
| Agent tool | Web fetch text |
See Detect and Prevent guides for architecture.
Limitations
- 32,000 character limit — split large documents
- Latency — add timeout via
AbortSignal; budget in UX - Fail-open vs fail-closed — API outages trade availability for security; document your choice
- Not a jailbreak solution alone — add output moderation where needed (injection vs jailbreak)
- Verdicts are risk signals — not guarantees of safe model behavior
Prototype payloads in Prompt Injection Checker.
Practical checklist
- Implement server-side
screenPromptInjectionwrapper with typed responses - Map
verdictto allow / review / block consistently - Log
request_id, categories, and action — not raw secrets - Screen RAG chunks separately from user messages
- Keep
IDENTICAPI_API_KEYserver-only - Handle API errors explicitly (503 fail-closed recommended)
- Add CI fixture tests against live or mocked API
- Review security checklist before production
TypeScript integration is straightforward: one API call per untrusted string, explicit verdict handling, and guards placed where text enters your server — not where it leaves the browser.
Frequently asked questions
Should detection run on the client or server?
Always server-side. Never expose API keys in browser code; call IdenticAPI from your backend or a Route Handler.
What HTTP status indicates a flagged prompt?
The API returns 200 with a verdict field. Your application decides whether to block based on safe, suspicious, or unsafe — and your own policy thresholds.
How do I handle API failures?
Fail closed for high-risk flows when possible: reject the request or queue for review rather than skipping screening silently.
Where is the official schema documented?
See /docs/prompt-injection-shield for request and response fields matching the production endpoint.
Related reading
- How to Detect Prompt Injection in LLM Applications
Practical methods to detect prompt injection before it reaches your model — heuristics, structural analysis, classificat…
- Prompt Injection Detection with Python
Call a prompt injection detection API from Python — request flow, environment variables, response handling, and integrat…
- How to Prevent Prompt Injection in Production AI Apps
Architectural controls, input validation, retrieval hardening, and layered defenses to reduce prompt injection risk in p…