Next.js Example
A complete, production-ready Next.js 15 API route with Guard protecting every layer — PII, injection, schema, budget, rate limiting, and audit logging.
Project Structure
app/
├── api/
│ └── chat/
│ └── route.ts ← AI route with Guard
├── lib/
│ └── guard.ts ← Shared Guardian instance
└── components/
└── Chat.tsx ← Frontend component
Shared Guardian Instance
// lib/guard.ts
import { Guardian } from '@edwinfom/ai-guard';
import { z } from 'zod';
import { db } from './db';
export const ResponseSchema = z.object({
answer: z.string(),
sources: z.array(z.string()).optional(),
confidence: z.number().min(0).max(1).optional(),
});
export const guard = new Guardian({
pii: {
targets: ['email', 'phone', 'creditCard', 'ssn', 'iban'],
onInput: true,
onOutput: true,
},
schema: {
validator: ResponseSchema,
repair: true,
},
injection: {
enabled: true,
sensitivity: 'medium',
},
canary: {
enabled: true,
throwOnLeak: true,
},
content: {
enabled: true,
categories: { toxicity: true, hate: true, violence: true, selfHarm: true },
},
budget: {
model: 'gpt-4o-mini',
maxCostUSD: 0.05,
maxTokens: 3000,
onWarning: (usage) => console.warn('Budget warning:', usage),
},
rateLimit: {
maxRequests: 20,
windowMs: 60_000,
keyFn: (_, ctx) => ctx?.userId ?? 'anonymous',
},
onAudit: async (entry) => {
await db.insert('ai_audit_log').values({
request_id: entry.requestId,
timestamp: new Date(entry.timestamp),
passed: entry.passed,
blocked_by: entry.blockedBy,
total_tokens: entry.meta.budget?.totalTokens ?? 0,
cost_usd: entry.meta.budget?.estimatedCostUSD ?? 0,
});
},
});API Route
// app/api/chat/route.ts
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
import { auth } from '@/lib/auth';
import { guard, ResponseSchema } from '@/lib/guard';
import {
InjectionError,
PiiError,
BudgetError,
RateLimitError,
ContentPolicyError,
CanaryError,
} from '@edwinfom/ai-guard';
const openai = new OpenAI();
export async function POST(req: NextRequest) {
const session = await auth();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { message } = await req.json();
if (!message?.trim()) {
return NextResponse.json({ error: 'Message is required' }, { status: 400 });
}
try {
const result = await guard.protect(
(safePrompt) =>
openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: 'You are a helpful assistant. Always respond in JSON format.',
},
{ role: 'user', content: safePrompt },
],
response_format: { type: 'json_object' },
}),
message,
{ userId: session.user.id }
);
return NextResponse.json({
data: result.data,
meta: {
tokens: result.meta.budget?.totalTokens,
cost: result.meta.budget?.estimatedCostUSD,
piiFound: result.meta.piiRedacted?.length ?? 0,
},
});
} catch (err) {
// Handle specific Guard errors
if (err instanceof InjectionError) return NextResponse.json({ error: 'Message blocked: security violation.' }, { status: 400 });
if (err instanceof ContentPolicyError) return NextResponse.json({ error: 'Message blocked: content policy violation.' }, { status: 400 });
if (err instanceof CanaryError) return NextResponse.json({ error: 'Security alert: system prompt compromised.' }, { status: 400 });
if (err instanceof RateLimitError) return NextResponse.json({ error: 'Too many requests. Please wait.' }, { status: 429 });
if (err instanceof BudgetError) return NextResponse.json({ error: 'Service temporarily unavailable.' }, { status: 503 });
console.error('Unexpected error:', err);
return NextResponse.json({ error: 'Internal server error.' }, { status: 500 });
}
}Frontend Component
// components/Chat.tsx
'use client';
import { useState } from 'react';
export default function Chat() {
const [messages, setMessages] = useState<{ role: string; content: string }[]>([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const sendMessage = async () => {
if (!input.trim()) return;
setError(null);
setLoading(true);
const userMsg = { role: 'user', content: input };
setMessages((prev) => [...prev, userMsg]);
setInput('');
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: input }),
});
const json = await res.json();
if (!res.ok) {
setError(json.error);
return;
}
setMessages((prev) => [
...prev,
{ role: 'assistant', content: json.data.answer },
]);
} catch {
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="chat-container">
<div className="messages">
{messages.map((m, i) => (
<div key={i} className={`message message--${m.role}`}>
{m.content}
</div>
))}
{loading && <div className="message message--loading">Thinking...</div>}
{error && <div className="message message--error">{error}</div>}
</div>
<div className="input-area">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && !loading && sendMessage()}
placeholder="Ask anything..."
disabled={loading}
/>
<button onClick={sendMessage} disabled={loading || !input.trim()}>
Send
</button>
</div>
</div>
);
}