How to Add AI Security to a SaaS API Route
Add AI security to SaaS API routes — middleware control flow, input guards, model calls, output checks, and tenant-aware policies.
SaaS products expose AI features through HTTP API routes — chat completions, document Q&A, summarization, and agent workflows. Security belongs inside those routes, after authentication and tenant resolution, as explicit middleware control flow — not as an optional client-side check or a single bolt-on at the edge.
This guide maps middleware placement for multi-tenant SaaS API routes: input guards, model calls, output checks, failure modes, and tenant-aware policy. For the full SaaS architecture picture, see AI SaaS Guardrails. For middleware concepts, see LLM Security Middleware.
Request lifecycle control flow
Every AI-enabled route should follow a predictable sequence:
HTTP request
→ AuthN (session / API key)
→ AuthZ + tenant resolution
→ Schema validation + rate limits
→ Assemble LLM context (history, RAG, system vars)
→ INPUT GUARD (Unified Guard)
→ Branch on decision: block | review | allow
→ LLM / RAG / tools (if allowed)
→ OUTPUT GUARD (Unified Guard)
→ Branch on decision
→ Response + metadata audit log
Guardrails execute after you know tenant_id and before side effects. WAF and CDN rules complement but do not replace in-route guards.
Middleware layers
| Layer | Responsibility | Fails if skipped |
|---|---|---|
| Edge (WAF, CDN) | DDoS, coarse IP blocks | App-layer injection, PII |
| API gateway | Auth, rate limit, request size | Tenant-scoped policy |
| Route handler | Assemble context, call guards | Wrong text scanned |
| Guard API | Classify risk | N/A — this is the classifier |
| Policy router | Map verdict → action | Inconsistent UX |
| Model client | Inference | N/A |
| Render/storage | Sanitize, persist | XSS, data leaks |
IdenticAPI is a caller-side guardrails API. Your middleware invokes POST /api/v1/guard; you retain direct model provider clients (middleware vs proxy).
Tenant-aware policy
Multi-tenant SaaS needs policy that varies by customer tier without forking code paths:
type TenantPolicy = {
failClosed: boolean;
inputChecks: ("prompt_injection" | "pii_secrets")[];
outputChecks: ("output_safety" | "pii_secrets")[];
redactPii: boolean;
reviewWebhook?: string;
};
const policies: Record<string, TenantPolicy> = {
enterprise: {
failClosed: true,
inputChecks: ["prompt_injection", "pii_secrets"],
outputChecks: ["output_safety", "pii_secrets"],
redactPii: true,
reviewWebhook: "https://internal.example/hooks/review"
},
standard: {
failClosed: false,
inputChecks: ["prompt_injection", "pii_secrets"],
outputChecks: ["output_safety"],
redactPii: true
}
};
Resolve policy from authenticated tenant record — never from client JSON bodies.
Enterprise tenants often require fail closed when guard APIs are unavailable (fail open vs fail closed).
Input middleware implementation
Express example
import type { Request, Response, NextFunction } from "express";
async function inputGuardMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const tenant = req.tenant; // set by prior auth middleware
const policy = getPolicy(tenant.id);
const assembled = buildPrompt(req.body, tenant);
try {
const guard = await callUnifiedGuard({
text: assembled,
checks: policy.inputChecks,
redact: policy.redactPii
});
req.guardInput = {
requestId: guard.request_id,
decision: guard.decision,
textForModel: extractRedactedText(guard) ?? assembled
};
if (guard.decision === "block") {
return res.status(400).json({
error: "request_blocked",
message: SAFE_FALLBACK,
request_id: guard.request_id
});
}
if (guard.decision === "review") {
await enqueueReview(tenant.id, guard.request_id);
return res.status(202).json({
error: "pending_review",
message: REVIEW_HOLD_MESSAGE,
request_id: guard.request_id
});
}
next();
} catch (err) {
if (policy.failClosed) {
return res.status(503).json({ error: "guard_unavailable" });
}
req.guardInput = { bypassed: true };
next();
}
}
Next.js Route Handler pattern
Place guards in Route Handlers or Server Actions — never in Client Components with exposed API keys (output safety in Next.js).
Sequence:
POST /api/chatauthenticates session- Input guard middleware runs on assembled prompt
- Handler calls OpenAI/Anthropic with
textForModel - Output guard runs on completion
- Returns JSON to client
Output middleware
Mirror input flow after inference:
async function outputGuard(
completion: string,
tenantId: string
): Promise<{ text: string; blocked: boolean; requestId: string }> {
const policy = getPolicy(tenantId);
const guard = await callUnifiedGuard({
text: completion,
checks: policy.outputChecks
});
if (guard.decision === "block") {
return { text: SAFE_FALLBACK, blocked: true, requestId: guard.request_id };
}
if (guard.decision === "review") {
await enqueueReview(tenantId, guard.request_id);
return { text: REVIEW_HOLD_MESSAGE, blocked: true, requestId: guard.request_id };
}
return { text: completion, blocked: false, requestId: guard.request_id };
}
Do not echo blocked completions in error payloads or client-visible fields.
RAG routes
Document Q&A routes add retrieve hooks before input guard:
retrieve → per-chunk scan → assemble → input guard → LLM → output guard
See secure RAG chatbot production. Authorization filters are mandatory at retrieve — shared indexes are a top SaaS failure mode.
Agent routes
Routes that execute tools need an additional gate:
model proposes tool → agent_action guard → execute or block
Submit tool proposals to Unified Guard with checks: ["agent_action"] and the agent_action payload (Agent Action Guard). Text guards do not validate SQL, email recipients, or delete operations.
Unified Guard call helper
type GuardCheck =
| "prompt_injection"
| "pii_secrets"
| "output_safety"
| "agent_action";
async function callUnifiedGuard(body: {
text?: string;
checks: GuardCheck[];
redact?: boolean;
agent_action?: Record<string, unknown>;
}) {
const res = await fetch("https://www.identicapi.com/api/v1/guard", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IDENTICAPI_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(5000)
});
if (!res.ok) throw new Error(`Guard HTTP ${res.status}`);
return res.json();
}
Reuse HTTP connections (keep-alive) in high-throughput routes to reduce latency (guardrails latency).
Error handling matrix
| Condition | Enterprise policy | Standard policy |
|---|---|---|
decision: block | 400 + safe message | 400 + safe message |
decision: review | 202 + queue | 202 or block per config |
| Guard timeout | 503 fail closed | 200 with monitoring alert |
| Guard 5xx | 503 fail closed | Allow with alert (document risk) |
| Invalid API key (your bug) | 500 internal | 500 internal |
Document policies in version-controlled config reviewed with security stakeholders.
Audit logging per route
Emit structured events after each guard call:
{
"event": "ai_guard_input",
"tenant_id": "tenant_acme",
"user_id": "usr_123",
"route": "POST /v1/chat",
"decision": "allow",
"request_id": "req_guard_abc",
"checks": ["prompt_injection", "pii_secrets"],
"finding_categories": [],
"latency_ms": 42
}
Do not log assembled prompts or completions in production. Log request_id for support correlation (privacy-safe logging).
API key hygiene
SaaS customers may embed your API keys in their integrations. Protect your guard and model keys:
- Server-side only — never bundle IdenticAPI keys in frontend JavaScript
- Per-environment keys with rotation runbooks
- Separate keys for CI vs production
Testing routes in CI
For each route, assert:
- Blocked input never reaches mocked LLM client
- Output guard replaces unsafe completions
- Tenant policy selects correct
checksarray - Fail-closed tenant gets 503 on guard outage
Summary
Add AI security to SaaS API routes with explicit middleware control flow: authenticate, resolve tenant, validate input, run Unified Guard on assembled context, branch on decision, call the model, run output guard, then respond. Vary policy by tenant tier, fail closed for enterprise when guards are unavailable, and keep authorization and rendering as separate layers.
Add guards to your API routes · AI SaaS guardrails architecture
Frequently asked questions
Where do AI guardrails belong in a SaaS API route?
After authentication and tenant resolution, before the LLM provider call (input guard), and again after inference before the HTTP response (output guard). Guards belong inside your API boundary, not only at the CDN or WAF edge.
How should multi-tenant SaaS vary guardrail policy?
Load policy from the authenticated tenant record — checks arrays, redact behavior, and fail-closed vs fail-open on guard outages. Never trust client-supplied tenant_id or policy fields without server verification.
What should happen when Unified Guard returns block?
Do not call the LLM on input block. Return a static safe error with request_id. On output block, replace the completion with a pre-approved fallback — never echo flagged model text in errors or logs visible to broad staff.
Do RAG API routes need different middleware?
Yes. Add per-chunk screening and authorization filters before assembling the prompt, then run the same input and output guard pattern as chat-only routes on the full assembled string.
How do agent-enabled SaaS routes differ?
Add a pre-execution gate: submit each proposed tool call to Unified Guard with checks [agent_action] and the agent_action payload before any side effect runs. Text guards do not validate SQL, email recipients, or delete operations.
Related reading
- Building Guardrails for an AI SaaS Product
End-to-end guardrails for AI SaaS — frontend to API, input security, LLM, output security, and agent checks in a multi-t…
- How to Add AI Guardrails to a Next.js App
Add AI guardrails to Next.js App Router apps — server-side Unified Guard calls, input/output checks, and keeping API key…
- How to Protect API Keys in SaaS Applications
Protect API keys in SaaS — server-side secrets, environment variables, rotation, log redaction, browser exposure risks, …