Integrating LLMs into Your Web App: A Practical Guide for Full Stack Developers
A hands-on 2026 guide for full stack developers covering Claude, GPT-4, and Gemini API integration — streaming, tool use, structured JSON output, prompt engineering, context caching, error handling, and token monitoring with TypeScript examples.
Integrating LLMs into Your Web App: A Practical Guide for Full Stack Developers
By Elhassane Mehdioui — Full Stack Web Developer & AI Consultant
Large Language Models are no longer a research curiosity. In 2026, they are infrastructure — as fundamental to a modern web app as a database or an authentication layer. Yet most tutorials stop at "call the API, render the response." Real production integrations involve streaming, tool use, cost management, error resilience, and observability. This guide covers all of it, with TypeScript examples you can drop into your Next.js, Express, or Node-based stack today.
Keywords: LLM web app integration, Claude GPT API integration, AI full stack 2026, LLM API tutorial
1. Choosing Your Model: Claude vs GPT-4 vs Gemini
Before writing a single line of code, you need to pick the right model for your use case. These three families dominate production usage and each has a distinct profile.
| Claude 3.5 Sonnet | GPT-4o | Gemini 1.5 Pro | |
|---|---|---|---|
| Context window | 200 000 tokens | 128 000 tokens | 1 000 000 tokens |
| Input cost (per 1M tokens) | ~$3 | ~$5 | ~$3.50 |
| Output cost (per 1M tokens) | ~$15 | ~$15 | ~$10.50 |
| Strengths | Instruction following, long-document reasoning, safe outputs | Broad general knowledge, code, vision | Very long context, multimodal, Google ecosystem |
| Tool use / function calling | Native, reliable | Native, mature | Native, improving |
| Structured JSON output | First-class support | response_format JSON mode | JSON mode via system prompt |
Rule of thumb for 2026: Use Claude when you need precise instruction following and safe, auditable outputs. Use GPT-4o when you need a proven, widely-documented API with rich tooling. Use Gemini 1.5 Pro when your use case involves very long documents (legal contracts, codebases, full books) or when you are already invested in Google Cloud.
2. Streaming Responses
Nothing kills UX faster than a spinner sitting for 8 seconds before text appears. Streaming fixes this by sending tokens as they are generated.
Claude (Anthropic SDK)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
export async function streamClaude(userMessage: string): Promise<void> {
const stream = await client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: userMessage }],
});
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
process.stdout.write(chunk.delta.text);
}
}
const finalMessage = await stream.finalMessage();
console.log("\nTotal tokens used:", finalMessage.usage.input_tokens + finalMessage.usage.output_tokens);
}
Next.js API Route with Server-Sent Events
// app/api/chat/route.ts
import { NextRequest } from "next/server";
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
export async function POST(req: NextRequest) {
const { message } = await req.json();
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
const stream = await client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 2048,
messages: [{ role: "user", content: message }],
});
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text: chunk.delta.text })}\n\n`));
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
3. Tool Use and Function Calling
Tool use lets the model decide when to call an external function — a database query, a web search, a calculator — and incorporate the result into its response. This is the foundation of agentic applications.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
name: "get_product_price",
description: "Retrieve the current price of a product from the catalog.",
input_schema: {
type: "object" as const,
properties: {
product_id: {
type: "string",
description: "The unique product identifier",
},
currency: {
type: "string",
enum: ["USD", "EUR", "GBP"],
description: "Currency for the price",
},
},
required: ["product_id"],
},
},
];
async function getProductPrice(productId: string, currency = "USD"): Promise<number> {
// Replace with your real database/API call
const prices: Record<string, number> = { "prod_001": 49.99, "prod_002": 129.00 };
return prices[productId] ?? 0;
}
export async function runWithTools(userQuery: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: userQuery },
];
while (true) {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools,
messages,
});
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
return textBlock?.type === "text" ? textBlock.text : "";
}
if (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const input = block.input as { product_id: string; currency?: string };
const price = await getProductPrice(input.product_id, input.currency);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify({ price, currency: input.currency ?? "USD" }),
});
}
}
messages.push({ role: "user", content: toolResults });
}
}
}
4. Structured JSON Output with Schemas
Asking a model to return JSON is unreliable without enforcement. Claude supports a json response format via tool use patterns, while OpenAI provides response_format. For maximum portability, use a Zod schema to validate whatever you receive.
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const ProductSchema = z.object({
name: z.string(),
price: z.number().positive(),
category: z.string(),
inStock: z.boolean(),
tags: z.array(z.string()),
});
type Product = z.infer<typeof ProductSchema>;
export async function extractProductData(rawText: string): Promise<Product> {
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 512,
system: `You are a data extraction assistant. Always respond with valid JSON matching this schema exactly:
{
"name": string,
"price": number,
"category": string,
"inStock": boolean,
"tags": string[]
}
Do not include any explanation or markdown — only the raw JSON object.`,
messages: [{ role: "user", content: rawText }],
});
const text = response.content[0];
if (text.type !== "text") throw new Error("Unexpected response type");
const parsed = JSON.parse(text.text);
return ProductSchema.parse(parsed); // throws ZodError if schema is violated
}
5. Prompt Engineering Patterns
Good prompts are software. Treat them with the same discipline as your application code.
System prompt structure that works:
const SYSTEM_PROMPT = `
You are a customer support assistant for Acme Corp, an e-commerce platform.
## Role
Answer questions about orders, returns, and product availability.
## Constraints
- Never fabricate order status. If you do not have data, say so.
- Keep responses under 150 words unless the user explicitly asks for detail.
- Always end with a follow-up question to confirm resolution.
## Tone
Professional, warm, direct. No filler phrases like "Certainly!" or "Of course!".
`.trim();
Few-shot examples for classification:
const FEW_SHOT_EXAMPLES = `
User: "My package hasn't arrived after 3 weeks"
Category: complaint | Urgency: high
User: "Do you ship to Canada?"
Category: inquiry | Urgency: low
User: "I want to cancel my order"
Category: cancellation | Urgency: medium
`;
Chain-of-thought for reasoning tasks:
const COT_SUFFIX = `
Think step by step before giving your final answer.
Wrap your reasoning in <thinking> tags and your answer in <answer> tags.
`;
6. Context Caching for Cost Reduction
When your system prompt or reference documents are large (5 000+ tokens), re-sending them with every request is wasteful. Anthropic's prompt caching can reduce costs by up to 90% on cached content.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const LARGE_DOCUMENT = `...your 50-page product manual here...`;
export async function queryWithCaching(userQuestion: string) {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: [
{
type: "text",
text: LARGE_DOCUMENT,
cache_control: { type: "ephemeral" }, // cache this block
},
{
type: "text",
text: "Answer user questions based solely on the document above.",
},
],
messages: [{ role: "user", content: userQuestion }],
});
console.log("Cache creation tokens:", response.usage.cache_creation_input_tokens);
console.log("Cache read tokens:", response.usage.cache_read_input_tokens);
return response;
}
The first request creates the cache; subsequent requests within the cache TTL (5 minutes for ephemeral) read from it at a fraction of the cost.
7. Error Handling and Fallbacks
Production LLM integrations fail in ways that are different from a REST API: rate limits, context length overflows, content policy blocks, and malformed JSON are all common. Build a resilience layer from day one.
import Anthropic from "@anthropic-ai/sdk";
interface LLMResponse {
text: string;
model: string;
cached: boolean;
}
async function callWithFallback(prompt: string): Promise<LLMResponse> {
const client = new Anthropic();
const models = ["claude-sonnet-4-6", "claude-haiku-4-5"];
for (const model of models) {
try {
const response = await client.messages.create({
model,
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
const textBlock = response.content.find((b) => b.type === "text");
if (!textBlock || textBlock.type !== "text") throw new Error("No text block");
return {
text: textBlock.text,
model,
cached: (response.usage.cache_read_input_tokens ?? 0) > 0,
};
} catch (err) {
if (err instanceof Anthropic.RateLimitError) {
console.warn(`Rate limited on ${model}, trying next...`);
await new Promise((r) => setTimeout(r, 2000));
continue;
}
if (err instanceof Anthropic.BadRequestError && model !== models[models.length - 1]) {
console.warn(`Bad request on ${model}, falling back...`);
continue;
}
throw err; // rethrow unknown errors
}
}
throw new Error("All models exhausted");
}
Key error types to handle:
RateLimitError(429) — exponential backoff, queue requestsBadRequestError(400) — often a context length overflow; truncate and retryAuthenticationError(401) — alert immediately, do not retryAPIError(5xx) — retry with backoff, switch model if persistentZodError— model returned invalid JSON; retry with stronger schema instruction
8. Token Monitoring
Unmonitored token usage is how AI projects blow their budgets. Instrument every call.
interface TokenMetrics {
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
cacheCreationTokens: number;
estimatedCostUsd: number;
}
const CLAUDE_SONNET_PRICING = {
input: 3.0 / 1_000_000,
output: 15.0 / 1_000_000,
cacheRead: 0.3 / 1_000_000,
cacheWrite: 3.75 / 1_000_000,
};
function extractMetrics(usage: Anthropic.Usage): TokenMetrics {
const inputTokens = usage.input_tokens;
const outputTokens = usage.output_tokens;
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
const cacheCreationTokens = usage.cache_creation_input_tokens ?? 0;
const estimatedCostUsd =
inputTokens * CLAUDE_SONNET_PRICING.input +
outputTokens * CLAUDE_SONNET_PRICING.output +
cacheReadTokens * CLAUDE_SONNET_PRICING.cacheRead +
cacheCreationTokens * CLAUDE_SONNET_PRICING.cacheWrite;
return { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, estimatedCostUsd };
}
// Usage
const response = await client.messages.create({ /* ... */ });
const metrics = extractMetrics(response.usage);
console.log(`Cost for this call: $${metrics.estimatedCostUsd.toFixed(6)}`);
// Persist to your observability stack (Datadog, PostHog, your own DB)
await logMetrics({ userId, sessionId, ...metrics });
Set up alerts when daily spend crosses a threshold. A runaway loop or a forgotten test that calls production APIs can cost hundreds of dollars overnight.
Putting It All Together
A production-grade LLM integration is not just an API call. It is a system with:
- Model selection logic aligned to task type and cost targets
- Streaming for responsive UX
- Tool use to ground responses in real data
- Schema validation to make outputs machine-readable
- Prompt engineering treated as versioned, tested software
- Context caching to cut costs on repeated large contexts
- Error handling with graceful degradation across model tiers
- Token observability wired into your existing monitoring stack
Start with a single feature — a support chatbot, a document summariser, a code review assistant — apply these patterns, and measure the results. The developers who build reliable, cost-efficient AI features in 2026 are not the ones chasing the newest model; they are the ones who instrument and iterate.
Elhassane Mehdioui is a Full Stack Web Developer and AI consultant specialising in LLM-powered product development. He works with startups and scale-ups to design and ship AI features that are production-ready from day one.