How to Add AI to an Existing App Without Breaking It
Adding AI to a production app is not a rewrite — it is a series of deliberate, reversible decisions made one at a time. The teams that succeed at LLM integration treat it the same way they treat any other infrastructure change: small surface area first, observable from day one, and always with a fallback. Here is exactly how I approach it at DanixSoft.
Why "AI Everywhere" Kills Projects
Every founder I talk to wants the full vision: AI that writes, summarises, generates, and explains across every surface in the app. I get it. But shipping that vision in one go is how you end up with six months of work, a broken user experience, and a model bill that makes your investors nervous.
When you try to add AI to an existing app in one shot, you accumulate risk in every dimension simultaneously — latency, cost, correctness, data privacy, and user trust. One bad prompt leaks a customer's PII. One cold model call adds 4 seconds to a page load. One unexpected token spike doubles your cloud bill.
The antidote is embarrassingly simple: pick one feature, ship it, learn, then expand.
Picking Your First Feature
A good first AI feature has three properties:
- High tolerance for imperfection. If the model gets it slightly wrong, the consequence is minor. Summarising a long support ticket is a good example. Auto-completing a legal clause is not.
- Bounded input size. You control how much text goes to the model. A feature that summarises a user's last five actions is safer than one that dumps your entire database schema into a prompt.
- Easy to measure. You can define "good" without ambiguity. A summary is good if users stop re-reading the original. You can track that with a simple click event.
Common first features that fit these criteria: search result summarisation, draft generation from a form submission, anomaly explanation in a dashboard, or a contextual help tooltip. Notice that none of these replace a core workflow — they augment one.
Build an Abstraction Layer First
Before you write a single prompt, build a provider abstraction. This is the single most important architectural decision you will make. It lets you swap from OpenAI to Anthropic to a self-hosted model without touching application code.
Here is a minimal TypeScript implementation:
// src/lib/ai/provider.ts
export interface AIMessage {
role: "system" | "user" | "assistant";
content: string;
}
export interface AIResponse {
text: string;
inputTokens: number;
outputTokens: number;
model: string;
}
export interface AIProvider {
complete(messages: AIMessage[], options?: CompletionOptions): Promise<AIResponse>;
stream(messages: AIMessage[], options?: CompletionOptions): AsyncIterable<string>;
}
export interface CompletionOptions {
model?: string;
maxTokens?: number;
temperature?: number;
signal?: AbortSignal;
}
// Concrete implementation — swap this class to change providers
export class AnthropicProvider implements AIProvider {
private client: Anthropic;
constructor(apiKey: string) {
this.client = new Anthropic({ apiKey });
}
async complete(messages: AIMessage[], options: CompletionOptions = {}): Promise<AIResponse> {
const system = messages.find((m) => m.role === "system")?.content ?? "";
const userMessages = messages.filter((m) => m.role !== "system");
const response = await this.client.messages.create({
model: options.model ?? "claude-sonnet-4-5",
max_tokens: options.maxTokens ?? 1024,
temperature: options.temperature ?? 0.3,
system,
messages: userMessages.map((m) => ({ role: m.role as "user" | "assistant", content: m.content })),
});
const block = response.content[0];
return {
text: block.type === "text" ? block.text : "",
inputTokens: response.usage.input_tokens,
outputTokens: response.usage.output_tokens,
model: response.model,
};
}
async *stream(messages: AIMessage[], options: CompletionOptions = {}): AsyncIterable<string> {
const system = messages.find((m) => m.role === "system")?.content ?? "";
const userMessages = messages.filter((m) => m.role !== "system");
const stream = this.client.messages.stream({
model: options.model ?? "claude-sonnet-4-5",
max_tokens: options.maxTokens ?? 1024,
system,
messages: userMessages.map((m) => ({ role: m.role as "user" | "assistant", content: m.content })),
});
for await (const event of stream) {
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
yield event.delta.text;
}
}
}
}
// Singleton — inject this everywhere instead of the SDK directly
export function createAIProvider(): AIProvider {
return new AnthropicProvider(process.env.ANTHROPIC_API_KEY!);
}
Your application code calls provider.complete() or provider.stream(). It never calls anthropic.messages.create() directly. When you need to switch models or providers, you change one file.
Managing Latency and Cost
LLMs are slow and can be expensive. You need a strategy before you launch, not after.
Streaming
Stream responses instead of waiting for completion. Users tolerate 300ms before the first token much better than 3 seconds of silence. Most modern frameworks support streaming natively. The stream() method in the abstraction above is not optional — it is your default for any user-facing feature.
Caching
Identical or near-identical prompts are surprisingly common. A dashboard that summarises the same weekly report for every user in the same org should hit a cache, not the model. Cache at the prompt hash level using Redis or your existing key-value store. Set a TTL that matches the staleness tolerance of the content — a summary of yesterday's data can be cached for 24 hours.
Model Tiering
Not every task needs your most capable model. A quick tone-check on a subject line does not need the same model as a detailed code explanation. Define tiers in your abstraction and route by task complexity:
- Tier 1 (fast, cheap): Classification, short completions, yes/no judgements — use a smaller or faster model.
- Tier 2 (balanced): Summarisation, draft generation, explanation — use a mid-tier model.
- Tier 3 (capable): Multi-step reasoning, code generation, long-form synthesis — use your most capable model.
Token Budgets
Set explicit max_tokens on every call. Never let the model run unbounded. Token budgets are both a cost control and a latency control — a response capped at 256 tokens returns faster and costs less than one allowed to run to 4096.
Reliability and Graceful Fallback
A model API will go down. A call will time out. A response will be malformed. Your app needs to handle all three without surfacing an error to the user.
The pattern I use is: try the primary model, fall back to a simpler model on failure, fall back to a deterministic non-AI response if both fail.
async function safeComplete(provider: AIProvider, messages: AIMessage[]): Promise<string> {
try {
const result = await Promise.race([
provider.complete(messages, { model: "claude-sonnet-4-5", maxTokens: 512 }),
timeout(6000), // 6-second hard limit
]);
return (result as AIResponse).text;
} catch {
// Log and continue — never let the AI path crash the request
logger.warn("AI completion failed, returning fallback");
return ""; // or a static fallback string appropriate for this feature
}
}
The fallback does not have to be clever. An empty string, a placeholder message, or hiding the AI widget entirely is always better than a 500 error.
Evals Before and After Launch
You need a test suite for AI behaviour the same way you have one for business logic. Before launch, build a small golden dataset: 20–50 representative inputs with acceptable outputs defined by a human. Run your prompt against this dataset and score it. This is your baseline.
After launch, log every prompt, response, and — critically — any user action that signals satisfaction or dissatisfaction (thumbs down, edit, re-generation). These signals become your ongoing eval dataset. Any prompt change or model upgrade should be measured against this dataset before it ships.
Do not skip this. It is the only way to catch regressions when you upgrade models or refine prompts.
Data Privacy: What Must Not Leave Your System
This is non-negotiable. Before any LLM integration goes to a third-party provider, audit what goes into your prompts.
Never send to a third-party model API:
- Personally identifiable information (PII) unless your DPA with the provider explicitly covers it and your privacy policy discloses it.
- Authentication tokens, API keys, or secrets — audit your context-building code carefully.
- Data your terms of service prohibit sharing with third parties.
- Health data (HIPAA-covered entities need a BAA in place before sending any PHI).
The practical approach: build a scrubbing step in your prompt construction pipeline. Replace names with tokens, strip email addresses, redact card numbers. Reconstruct the original context only on the way back in, after the model has responded. If scrubbing is not feasible for the feature, use a self-hosted or on-premises model instead.
Rolling Out Safely with Feature Flags and Monitoring
Never ship an AI feature to 100% of users on day one. Use a feature flag to control rollout.
Start at 1–5% of users, monitor the key signals (latency p95, error rate, cost per request, user satisfaction signal), then expand. If any metric degrades, kill the flag. This gives you a blast radius of one percent instead of one hundred.
Instrument every AI call with the same observability you apply to any critical path: trace ID, model name, token counts, latency, cache hit or miss, and fallback triggered or not. These metrics will tell you where to optimise before cost or latency becomes a real problem.
A feature flag also lets you A/B test prompts. Run two prompt variants against separate cohorts and measure which produces better user outcomes. This is how you improve quality without guessing.
Key Takeaways
- Start with one high-value, low-risk feature. Expand after you understand the cost, latency, and quality profile.
- Build a provider abstraction before writing your first prompt. Swapping models is inevitable.
- Stream by default. Cache aggressively. Set token budgets on every call.
- Plan your fallback before you plan your happy path. The model will fail.
- Build a golden eval dataset before launch and measure every change against it.
- Audit your prompts for PII and secrets. What you send to a third-party API is a data handling decision, not just a technical one.
- Roll out with a feature flag. Ship to 1–5%, monitor, then expand.
LLM integration done right is boring infrastructure work with a good product outcome. The goal is an AI feature users trust — not one that occasionally surprises them in the wrong direction.