Developer Documentation ⏱️ 36 min read Verified Production Ready

Modern JS & TS for AI Applications — Codebitbybit

TypeScript deep dive covering strict type safety, generics, Zod schema validation, async iterators, and AI SDK integrations.

docs / js-ts-ai-applications
v2.0 · 2025 Updated

Modern JavaScript & TypeScript
for AI Applications

This is not a language tutorial. This is an engineering guide for developers building AI-powered applications — from real-time chat interfaces to backend orchestration layers, from structured prompt pipelines to production-grade TypeScript systems.

Node.js 20+ TypeScript 5 AI APIs Next.js Production Full-Stack

Why JS/TS is the dominant language for AI apps

⚡
Async by Design
Native async/await and event-loop architecture match perfectly with AI API latency patterns — streaming, polling, and long-running inference.
🌐
Universal Runtime
One language handles browser UI, Node.js API servers, edge functions, and CLI tools. Reduce context-switching across the entire AI stack.
🔌
Richest SDK Ecosystem
Every major AI provider ships a first-party JS/TS SDK — Anthropic, OpenAI, Google, Mistral, Cohere, HuggingFace. The best tooling is here first.
🏗️
Full-Stack Coherence
Share types, validation schemas, and utility functions between your Next.js frontend and Express/Hono backend without friction.
🔒
TypeScript Safety
Strongly-typed API contracts for AI prompts, response schemas, and tool definitions catch bugs before they reach production.
🚀
Edge Native
Deploy AI inference proxies on Cloudflare Workers, Vercel Edge, or Deno Deploy — millisecond cold starts with V8 isolates.
💡
Engineering mindset. This guide assumes you know JavaScript. We focus on patterns, architecture, and production concerns — not syntax basics. If you see ?.optional or ??nullish and wonder what they do, MDN is one tab away.
JavaScript's Role in the AI Stack

JavaScript operates at every layer of a modern AI application. Understanding which layer handles what work is the foundation of good AI app architecture.

The AI Application Stack

01 User Interface Layer React · Next.js · Svelte
↕ HTTP / WebSocket / SSE
02 API Gateway / BFF Next.js API · Hono · Express
↕ SDK calls
03 AI Orchestration Layer Prompt pipelines · RAG · Agents
↕ REST / gRPC
04 AI Model Provider Anthropic · OpenAI · Gemini

Frontend: The User's AI Interface

The frontend orchestrates user interaction, streams responses, manages conversation state, and renders AI output in real time. It must handle partial JSON, markdown, code blocks, and tool call results gracefully.

typescript · react component
// AIStreamChat.tsx — Production streaming chat component
import { useState, useRef, useCallback } from 'react';

interface Message {
  role: 'user' | 'assistant';
  content: string;
  id: string;
}

export function AIStreamChat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput]       = useState('');
  const [streaming, setStreaming] = useState(false);
  const abortRef = useRef<AbortController | null>(null);

  const sendMessage = useCallback(async () => {
    if (!input.trim() || streaming) return;

    const userMsg: Message = {
      id: crypto.randomUUID(),
      role: 'user',
      content: input.trim(),
    };

    setMessages(prev => [...prev, userMsg]);
    setInput('');
    setStreaming(true);

    // Placeholder for streaming assistant message
    const assistantId = crypto.randomUUID();
    setMessages(prev => [...prev, { id: assistantId, role: 'assistant', content: '' }]);

    abortRef.current = new AbortController();

    try {
      const res = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ messages: [...messages, userMsg] }),
        signal: abortRef.current.signal,
      });

      if (!res.ok) throw new Error(`API error: ${res.status}`);
      if (!res.body) throw new Error('No response body');

      const reader = res.body.getReader();
      const decoder = new TextDecoder();

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value, { stream: true });
        // Parse SSE data chunks
        const lines = chunk.split('\n').filter(l => l.startsWith('data: '));

        for (const line of lines) {
          const text = line.slice(6); // strip "data: "
          if (text === '[DONE]') break;

          try {
            const parsed = JSON.parse(text);
            const delta = parsed.choices?.[0]?.delta?.content ?? '';
            setMessages(prev =>
              prev.map(m =>
                m.id === assistantId
                  ? { ...m, content: m.content + delta }
                  : m
              )
            );
          } catch {
            // skip malformed chunks
          }
        }
      }
    } catch (err) {
      if ((err as Error).name !== 'AbortError') {
        setMessages(prev =>
          prev.map(m =>
            m.id === assistantId
              ? { ...m, content: '⚠️ Stream error. Please retry.' }
              : m
          )
        );
      }
    } finally {
      setStreaming(false);
    }
  }, [input, messages, streaming]);

  return (
    <div className="chat-container">
      {messages.map(msg => (
        <div key={msg.id} className={`message ${msg.role}`}>
          {msg.content || (streaming ? '▋' : '')}
        </div>
      ))}
      <div className="input-row">
        <input value={input} onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && !e.shiftKey && sendMessage()}
          placeholder="Send a message..." />
        <button onClick={streaming ? () => abortRef.current?.abort() : sendMessage}>
          {streaming ? 'Stop' : 'Send'}
        </button>
      </div>
    </div>
  );
}

Backend: The AI Orchestration Server

Node.js handles prompt construction, model routing, rate limiting, context management, streaming relay, and caching. It's the control plane for your AI operations.

typescript · node.js api route
// app/api/chat/route.ts — Next.js App Router streaming endpoint
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';
import { NextRequest } from 'next/server';

const client = new Anthropic(); // uses ANTHROPIC_API_KEY env var

const RequestSchema = z.object({
  messages: z.array(z.object({
    role: z.enum(['user', 'assistant']),
    content: z.string().min(1).max(32_000),
  })).min(1).max(100),
  model: z.string().optional().default('claude-sonnet-4-20250514'),
});

export async function POST(req: NextRequest) {
  const body = await req.json().catch(() => null);
  const parsed = RequestSchema.safeParse(body);

  if (!parsed.success) {
    return new Response(JSON.stringify({ error: parsed.error.flatten() }), {
      status: 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  const { messages, model } = parsed.data;

  const stream = client.messages.stream({
    model,
    max_tokens: 4096,
    system: 'You are a precise, expert AI assistant.',
    messages,
  });

  // Relay as SSE
  const encoder = new TextEncoder();
  const readable = new ReadableStream({
    async start(controller) {
      try {
        for await (const event of stream) {
          if (event.type === 'content_block_delta' &&
              event.delta.type === 'text_delta') {
            const sseChunk = `data: ${JSON.stringify({
              choices: [{ delta: { content: event.delta.text } }]
            })}\n\n`;
            controller.enqueue(encoder.encode(sseChunk));
          }
        }
        controller.enqueue(encoder.encode('data: [DONE]\n\n'));
      } catch (err) {
        controller.error(err);
      } finally {
        controller.close();
      }
    },
  });

  return new Response(readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'X-Accel-Buffering': 'no',
    },
  });
}
TypeScript in AI-Powered Systems

TypeScript isn't optional in production AI apps. It's the mechanism that enforces contracts between your prompt definitions, API responses, and downstream consumers.

Why TypeScript Matters More with AI

✗ UNTYPED — fragile
// You have no idea what comes back
const res = await callAI(prompt);
const name = res.user.profile.name;
// 💥 TypeError at runtime
✓ TYPED — safe
// Contract-first design
const res: AIUserResponse = await callAI(prompt);
const name = res.user?.profile?.name ?? 'Unknown';
// ✅ Caught at compile time

Core TypeScript Patterns for AI Apps

typescript · type definitions for AI systems
// types/ai.ts — Central type definitions for your AI layer

// ─── Message types ───────────────────────────────────────────────
export type MessageRole = 'user' | 'assistant' | 'system';

export interface ChatMessage {
  role:      MessageRole;
  content:   string;
  timestamp: number;
  id:        string;
  metadata?: Record<string, unknown>;
}

// ─── Model configuration ─────────────────────────────────────────
export interface ModelConfig {
  model:       'claude-sonnet-4-20250514' | 'claude-opus-4-20250514' | string;
  maxTokens:   number;
  temperature?: number;    // 0–1 for most models
  topP?:        number;
  system?:      string;
}

// ─── Prompt templates with type safety ───────────────────────────
export interface PromptTemplate<TInput extends Record<string, string>> {
  name:      string;
  template:  string;
  variables: (keyof TInput)[];
  render:    (input: TInput) => string;
}

// Generic factory for typed prompt templates
export function createPrompt<T extends Record<string, string>>(
  name: string,
  template: string,
  variables: (keyof T)[]
): PromptTemplate<T> {
  return {
    name,
    template,
    variables,
    render: (input: T) =>
      variables.reduce(
        (acc, key) => acc.replace(`{{${String(key)}}}`, input[key]),
        template
      ),
  };
}

// Usage:
const summarizePrompt = createPrompt<{ document: string; maxWords: string }>(
  'summarize',
  'Summarize the following in {{maxWords}} words or fewer:\n\n{{document}}',
  ['document', 'maxWords']
);

const rendered = summarizePrompt.render({
  document: '...',
  maxWords: '100'
}); // ← TypeScript validates both keys exist

// ─── AI tool definitions (function calling) ──────────────────────
export interface AITool<TInput, TOutput> {
  name:        string;
  description: string;
  inputSchema: z.ZodType<TInput>;
  execute:     (input: TInput) => Promise<TOutput>;
}

// ─── Structured output via Zod ────────────────────────────────────
import { z } from 'zod';

export const SentimentSchema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral', 'mixed']),
  score:     z.number().min(-1).max(1),
  reasoning: z.string(),
  entities:  z.array(z.string()).optional(),
});

export type SentimentAnalysis = z.infer<typeof SentimentSchema>;

// Force structured JSON output from AI
export async function analyzeSentiment(
  client: Anthropic,
  text: string
): Promise<SentimentAnalysis> {
  const response = await client.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 512,
    system: 'You respond ONLY with valid JSON matching the requested schema.',
    messages: [{
      role: 'user',
      content: `Analyze sentiment of: "${text}"\n\nRespond with JSON: { sentiment, score, reasoning, entities? }`
    }]
  });

  const raw = response.content[0].type === 'text'
    ? response.content[0].text
    : '';

  return SentimentSchema.parse(JSON.parse(raw));
}

Generics for Reusable AI Utilities

typescript · generic AI result wrapper
// A typed Result monad for AI operations — no thrown exceptions
type AIResult<T> =
  | { success: true;  data: T;       usage: TokenUsage }
  | { success: false; error: string; code: AIErrorCode };

type TokenUsage = { input: number; output: number; total: number };
type AIErrorCode = 'rate_limit' | 'context_overflow' | 'invalid_response' | 'timeout' | 'unknown';

// Wrapper that never throws
async function safeAICall<T>(
  fn: () => Promise<T>,
  usage?: TokenUsage
): Promise<AIResult<T>> {
  try {
    const data = await fn();
    return { success: true, data, usage: usage ?? { input: 0, output: 0, total: 0 } };
  } catch (err: unknown) {
    if (err instanceof Anthropic.RateLimitError) {
      return { success: false, error: 'Rate limit exceeded', code: 'rate_limit' };
    }
    if (err instanceof Anthropic.BadRequestError) {
      return { success: false, error: 'Context too large', code: 'context_overflow' };
    }
    const message = err instanceof Error ? err.message : 'Unknown error';
    return { success: false, error: message, code: 'unknown' };
  }
}

// Caller uses discriminated union — no runtime surprises
const result = await safeAICall(() => runInference(prompt));
if (!result.success) {
  if (result.code === 'rate_limit') await delay(1000);
  logError(result.error);
  return;
}
// ✅ TypeScript knows result.data is defined here
process(result.data);
Modern JS Patterns You'll Actually Use

Skip the beginner stuff. These are the ES2020+ patterns that appear repeatedly in AI application codebases.

Destructuring & Optional Chaining in API Responses

javascript · destructuring patterns
// Deep destructuring of AI API responses
const {
  id,
  model,
  usage: { input_tokens, output_tokens },
  content: [{ type, text } = {}],
  stop_reason = 'unknown',
} = response;

// Optional chaining — safe navigation through uncertain AI output
const firstCodeBlock = response
  ?.content
  ?.find(block => block.type === 'code')
  ?.code
  ?? '';  // ← Nullish coalescing: fallback if null/undefined (not 0 or '')

// Logical assignment operators (ES2021)
config.systemPrompt ??= 'You are a helpful assistant.'; // assign if nullish
config.temperature  ||= 0.7;  // assign if falsy
config.maxRetries   &&= Math.min(config.maxRetries, 5); // assign if truthy

// Object.fromEntries for transforming AI tool results
const toolResultsMap = Object.fromEntries(
  toolResults.map(({ name, output }) => [name, output])
);

Array Methods for Data Pipelines

javascript · functional array operations
// Build a conversation history from raw events
const conversationHistory = rawEvents
  .filter(e => e.type === 'message' && e.content.trim())
  .map(({ role, content, timestamp }) => ({ role, content, timestamp }))
  .slice(-20); // keep last 20 turns (context window management)

// Chunk documents for embedding / RAG pipelines
function chunkText(text, chunkSize = 512, overlap = 64) {
  const words  = text.split(/\s+/);
  const chunks = [];
  let   i = 0;

  while (i < words.length) {
    chunks.push(words.slice(i, i + chunkSize).join(' '));
    i += chunkSize - overlap;
  }

  return chunks;
}

// Group messages by session with reduce
const sessions = messages.reduce((acc, msg) => {
  (acc[msg.sessionId] ??= []).push(msg);
  return acc;
}, {});

// flatMap — flatten tool call results from parallel executions
const allResults = toolCallBatches
  .flatMap(batch => batch.results)
  .filter(r => r.status === 'success');

Module Patterns for AI Services

typescript · module design
// lib/ai/client.ts — Singleton client with lazy initialization
import Anthropic from '@anthropic-ai/sdk';

let _client: Anthropic | null = null;

export function getAIClient(): Anthropic {
  if (!_client) {
    if (!process.env.ANTHROPIC_API_KEY) {
      throw new Error('ANTHROPIC_API_KEY is required');
    }
    _client = new Anthropic({
      apiKey: process.env.ANTHROPIC_API_KEY,
      maxRetries: 3,
      timeout: 60_000,
    });
  }
  return _client;
}

// Named exports for tree-shaking
export { createPrompt, type PromptTemplate } from './prompts';
export { withRetry, withCache }             from './middleware';
export { RateLimiter }                      from './rate-limiter';

// Re-export types only (zero runtime cost)
export type { ChatMessage, ModelConfig, AIResult } from '../types/ai';
Async Mastery for AI Applications

AI APIs are fundamentally asynchronous — inference takes time, streams arrive in chunks, and parallel tool calls need coordination. Mastering async is non-negotiable.

⚠️
Critical pattern: Never await inside a loop when calls are independent. Use Promise.all() for parallelism, Promise.allSettled() when partial failure is acceptable.

Parallel vs Sequential AI Calls

typescript · concurrency patterns
// ❌ Sequential — takes N * latency
async function analyzeDocsBad(docs: string[]) {
  const results = [];
  for (const doc of docs) {
    results.push(await analyzeDoc(doc)); // waits each time
  }
  return results;
}

// ✅ Parallel — takes ~1 * latency
async function analyzeDocsGood(docs: string[]) {
  return Promise.all(docs.map(doc => analyzeDoc(doc)));
}

// ✅ Parallel with partial failure tolerance
async function analyzeDocsSafe(docs: string[]) {
  const settled = await Promise.allSettled(docs.map(analyzeDoc));
  return settled.map((result, i) => ({
    doc:    docs[i],
    status: result.status,
    data:   result.status === 'fulfilled' ? result.value : null,
    error:  result.status === 'rejected'  ? result.reason.message : null,
  }));
}

// ✅ Controlled concurrency — avoid rate limit errors
async function analyzeWithConcurrency(
  docs: string[],
  concurrency = 5
) {
  const results: Awaited<ReturnType<typeof analyzeDoc>>[] = [];

  for (let i = 0; i < docs.length; i += concurrency) {
    const batch  = docs.slice(i, i + concurrency);
    const batchResults = await Promise.all(batch.map(analyzeDoc));
    results.push(...batchResults);
    // Rate limit: pause between batches
    if (i + concurrency < docs.length) await delay(200);
  }

  return results;
}

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

Retry Logic with Exponential Backoff

typescript · retry middleware
interface RetryOptions {
  maxAttempts:   number;
  baseDelayMs:   number;
  maxDelayMs:    number;
  retryable?:    (error: unknown) => boolean;
  onRetry?:      (attempt: number, error: unknown) => void;
}

async function withRetry<T>(
  fn: () => Promise<T>,
  opts: RetryOptions
): Promise<T> {
  const {
    maxAttempts, baseDelayMs, maxDelayMs,
    retryable = isRetryable,
    onRetry,
  } = opts;

  let lastError: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (attempt === maxAttempts || !retryable(error)) throw error;

      // Exponential backoff with jitter
      const expDelay   = baseDelayMs * Math.pow(2, attempt - 1);
      const jitter     = Math.random() * baseDelayMs;
      const waitMs     = Math.min(expDelay + jitter, maxDelayMs);

      onRetry?.(attempt, error);
      await delay(waitMs);
    }
  }

  throw lastError;
}

function isRetryable(error: unknown): boolean {
  if (error instanceof Anthropic.RateLimitError)       return true;
  if (error instanceof Anthropic.InternalServerError)  return true;
  if (error instanceof Anthropic.APIConnectionError)   return true;
  return false;
}

// Usage
const result = await withRetry(
  () => client.messages.create({ model, messages, max_tokens: 1024 }),
  { maxAttempts: 4, baseDelayMs: 500, maxDelayMs: 15_000,
    onRetry: (n, e) => console.warn(`Retry ${n}:`, e) }
);

Server-Sent Events (SSE) Parsing

typescript · SSE stream consumer
// Parse SSE streams from any AI provider (agnostic)
async function* parseSSEStream(
  response: Response
): AsyncGenerator<string> {
  if (!response.body) throw new Error('Empty response body');

  const reader  = response.body.getReader();
  const decoder = new TextDecoder();
  let   buffer  = '';

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() ?? ''; // keep incomplete line

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;
        const data = line.slice(6).trim();
        if (data === '[DONE]') return;
        if (data) yield data;
      }
    }
  } finally {
    reader.releaseLock();
  }
}

// Consumer
for await (const rawChunk of parseSSEStream(response)) {
  const event = JSON.parse(rawChunk);
  process.stdout.write(event?.choices?.[0]?.delta?.content ?? '');
}
AI API Integration Patterns

Direct API integration gives you full control. These patterns cover Anthropic's API in depth, with patterns that transfer to any major provider.

Provider SDK Package Streaming Tool Use Vision
Anthropic@anthropic-ai/sdk✅ SSE✅ Native✅
OpenAIopenai✅ SSE✅ Native✅
Google Gemini@google/generative-ai✅✅✅
Mistral@mistralai/mistraljs✅✅✅
Vercel AI SDKai✅ Unified✅ Unified✅

Prompt Engineering in Code

typescript · prompt construction
// Structured prompt builder — maintainable, testable
class PromptBuilder {
  private systemParts: string[] = [];
  private userParts:   string[] = [];

  role(instruction: string) {
    this.systemParts.push(`## Role\n${instruction}`);
    return this;
  }

  context(ctx: string) {
    this.systemParts.push(`## Context\n${ctx}`);
    return this;
  }

  constraints(rules: string[]) {
    const list = rules.map((r, i) => `${i + 1}. ${r}`).join('\n');
    this.systemParts.push(`## Constraints\n${list}`);
    return this;
  }

  outputFormat(format: string) {
    this.systemParts.push(`## Output Format\n${format}`);
    return this;
  }

  userMessage(content: string) {
    this.userParts.push(content);
    return this;
  }

  build(): { system: string; userContent: string } {
    return {
      system:      this.systemParts.join('\n\n'),
      userContent: this.userParts.join('\n\n'),
    };
  }
}

// Usage
const { system, userContent } = new PromptBuilder()
  .role('You are an expert code reviewer specializing in TypeScript and Node.js.')
  .constraints([
    'Focus only on bugs, security issues, and performance problems.',
    'Ignore formatting and stylistic preferences.',
    'Rank issues by severity: critical, high, medium, low.',
    'Respond in JSON only.',
  ])
  .outputFormat('{ issues: [{ severity, line, description, fix }] }')
  .userMessage(`Review this code:\n\`\`\`typescript\n${codeToReview}\n\`\`\``)
  .build();

const response = await client.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 2048,
  system,
  messages: [{ role: 'user', content: userContent }],
});

Tool Use (Function Calling)

typescript · tool use / function calling
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

// Define tools with strict JSON Schema
const tools: Anthropic.Tool[] = [
  {
    name: 'search_database',
    description: 'Query the product database for items matching criteria.',
    input_schema: {
      type: 'object',
      properties: {
        query:    { type: 'string', description: 'Search query' },
        category: { type: 'string', enum: ['electronics', 'books', 'clothing'] },
        maxPrice: { type: 'number', minimum: 0 },
        limit:    { type: 'integer', minimum: 1, maximum: 50, default: 10 },
      },
      required: ['query'],
    },
  },
  {
    name: 'calculate_shipping',
    description: 'Estimate shipping cost for a product.',
    input_schema: {
      type: 'object',
      properties: {
        productId: { type: 'string' },
        zipCode:   { type: 'string', pattern: '^[0-9]{5}$' },
        expedited: { type: 'boolean', default: false },
      },
      required: ['productId', 'zipCode'],
    },
  },
];

// Agentic loop — model calls tools until it reaches final answer
async function runAgent(userQuery: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: 'user', content: userQuery }
  ];

  while (true) {
    const response = await client.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 4096,
      tools,
      messages,
    });

    // Add assistant's response to history
    messages.push({ role: 'assistant', content: response.content });

    if (response.stop_reason === 'end_turn') {
      // Extract final text response
      const textBlock = response.content.find(b => b.type === 'text');
      return textBlock?.text ?? '';
    }

    if (response.stop_reason === 'tool_use') {
      const toolResults: Anthropic.ToolResultBlockParam[] = [];

      for (const block of response.content) {
        if (block.type !== 'tool_use') continue;

        const result = await executeToolCall(block.name, block.input);
        toolResults.push({
          type:        'tool_result',
          tool_use_id: block.id,
          content:     JSON.stringify(result),
        });
      }

      messages.push({ role: 'user', content: toolResults });
    }
  }
}

// Tool executor — routes to real implementations
async function executeToolCall(
  name: string,
  input: Record<string, unknown>
): Promise<unknown> {
  switch (name) {
    case 'search_database':    return await db.search(input as SearchParams);
    case 'calculate_shipping': return await shipping.estimate(input as ShippingParams);
    default: throw new Error(`Unknown tool: ${name}`);
  }
}
Building AI-Powered UI Experiences

AI-integrated frontends demand new interaction patterns — streaming text rendering, optimistic UI, loading skeletons, abort control, and graceful degradation under latency.

Custom Hook: useAIStream

typescript · react custom hook
// hooks/useAIStream.ts — Reusable streaming hook
import { useState, useRef, useCallback } from 'react';

interface UseAIStreamOptions {
  endpoint: string;
  onChunk?: (chunk: string) => void;
  onComplete?: (fullText: string) => void;
  onError?: (error: Error) => void;
}

interface UseAIStreamReturn {
  text:      string;
  streaming: boolean;
  error:     string | null;
  stream:    (payload: unknown) => Promise<void>;
  abort:     () => void;
  reset:     () => void;
}

export function useAIStream({
  endpoint,
  onChunk,
  onComplete,
  onError,
}: UseAIStreamOptions): UseAIStreamReturn {
  const [text,      setText]      = useState('');
  const [streaming, setStreaming] = useState(false);
  const [error,     setError]     = useState<string | null>(null);
  const abortRef = useRef<AbortController | null>(null);
  const fullTextRef = useRef('');

  const stream = useCallback(async (payload: unknown) => {
    setText('');
    setError(null);
    setStreaming(true);
    fullTextRef.current = '';

    abortRef.current = new AbortController();

    try {
      const res = await fetch(endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
        signal: abortRef.current.signal,
      });

      if (!res.ok) {
        const errBody = await res.json().catch(() => ({}));
        throw new Error(errBody.message ?? `HTTP ${res.status}`);
      }

      for await (const chunk of parseSSEStream(res)) {
        try {
          const data  = JSON.parse(chunk);
          const delta = data?.choices?.[0]?.delta?.content
                     ?? data?.delta?.text
                     ?? '';
          if (delta) {
            fullTextRef.current += delta;
            setText(prev => prev + delta);
            onChunk?.(delta);
          }
        } catch { /* skip malformed */ }
      }

      onComplete?.(fullTextRef.current);
    } catch (err: unknown) {
      if ((err as Error).name === 'AbortError') return;
      const msg = err instanceof Error ? err.message : 'Stream failed';
      setError(msg);
      onError?.(err instanceof Error ? err : new Error(msg));
    } finally {
      setStreaming(false);
    }
  }, [endpoint, onChunk, onComplete, onError]);

  const abort = useCallback(() => {
    abortRef.current?.abort();
    setStreaming(false);
  }, []);

  const reset = useCallback(() => {
    setText(''); setError(null);
  }, []);

  return { text, streaming, error, stream, abort, reset };
}

AI Response Rendering Patterns

typescript · markdown streaming renderer
// components/StreamingMarkdown.tsx
import { useMemo } from 'react';
import { marked }  from 'marked';
import DOMPurify   from 'dompurify';

interface Props {
  content:   string;
  streaming: boolean;
}

// Configure marked for safe rendering
marked.setOptions({
  gfm:    true,
  breaks: true,
});

export function StreamingMarkdown({ content, streaming }: Props) {
  // Only re-parse when content changes (memoized)
  const html = useMemo(() => {
    const raw = marked.parse(content) as string;
    return DOMPurify.sanitize(raw, {
      ALLOWED_TAGS: ['p', 'h1','h2','h3','strong','em','code',
                     'pre','ul','ol','li','blockquote','a'],
      ALLOWED_ATTR: ['href', 'target', 'rel'],
    });
  }, [content]);

  return (
    <div className="prose prose-invert max-w-none">
      <div dangerouslySetInnerHTML={{ __html: html }} />
      {streaming && (
        <span
          className="inline-block w-[2px] h-[1em] bg-current ml-0.5 animate-pulse"
          aria-hidden="true"
        />
      )}
    </div>
  );
}
Building AI-Backed API Services

Your Node.js backend is the trust boundary for your AI application — it holds the API keys, enforces rate limits, validates inputs, and routes between model providers.

Hono.js — Lightweight API Server for AI

typescript · hono.js api server
import { Hono }        from 'hono';
import { cors }        from 'hono/cors';
import { logger }      from 'hono/logger';
import { rateLimiter } from 'hono-rate-limiter';
import { zValidator }  from '@hono/zod-validator';
import { z }           from 'zod';
import Anthropic       from '@anthropic-ai/sdk';

const app    = new Hono();
const client = new Anthropic();

// ─── Global middleware ───────────────────────────────────────────
app.use('*', logger());
app.use('*', cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') ?? '*' }));
app.use('/api/*', rateLimiter({
  windowMs: 60_000,  // 1 minute
  limit:    20,       // 20 requests per user per minute
  keyGenerator: (c) => c.req.header('x-user-id') ?? c.req.raw.headers.get('cf-connecting-ip') ?? 'unknown',
}));

// ─── Auth middleware ─────────────────────────────────────────────
app.use('/api/*', async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '');
  if (!token || !isValidToken(token)) {
    return c.json({ error: 'Unauthorized' }, 401);
  }
  c.set('userId', decodeToken(token).userId);
  await next();
});

// ─── Chat completion route ───────────────────────────────────────
const ChatSchema = z.object({
  messages: z.array(z.object({
    role:    z.enum(['user', 'assistant']),
    content: z.string().min(1).max(16_000),
  })).min(1).max(50),
  stream: z.boolean().optional().default(true),
  model:  z.string().optional(),
});

app.post(
  '/api/chat',
  zValidator('json', ChatSchema),
  async (c) => {
    const { messages, stream, model } = c.req.valid('json');
    const userId = c.get('userId');

    if (!stream) {
      // Non-streaming response
      const response = await client.messages.create({
        model:      model ?? 'claude-sonnet-4-20250514',
        max_tokens: 4096,
        messages,
      });
      return c.json({ content: response.content, usage: response.usage });
    }

    // Streaming response
    return c.stream(async (stream) => {
      const aiStream = client.messages.stream({
        model:      model ?? 'claude-sonnet-4-20250514',
        max_tokens: 4096,
        messages,
      });

      for await (const event of aiStream) {
        if (event.type === 'content_block_delta' &&
            event.delta.type === 'text_delta') {
          await stream.write(
            `data: ${JSON.stringify({ delta: event.delta.text })}\n\n`
          );
        }
      }
      await stream.write('data: [DONE]\n\n');
    }, 200, {
      'Content-Type':  'text/event-stream',
      'Cache-Control': 'no-cache',
    });
  }
);

// ─── Health + metrics ────────────────────────────────────────────
app.get('/health', (c) => c.json({ status: 'ok', ts: Date.now() }));

export default app;

Context & Session Management

typescript · session + context manager
// lib/context-manager.ts
// Context window management: keep cost under control without losing coherence

const TOKEN_BUDGET    = 180_000; // Leave headroom in 200k window
const SUMMARY_TRIGGER = 80_000;  // Summarize when context exceeds this

class ContextManager {
  private sessions = new Map<string, ConversationSession>();

  async getContext(sessionId: string): Promise<ChatMessage[]> {
    const session = this.sessions.get(sessionId);
    if (!session) return [];
    return session.messages;
  }

  async addMessage(
    sessionId: string,
    message: ChatMessage
  ): Promise<void> {
    const session = this.sessions.get(sessionId) ?? { messages: [] };
    session.messages.push(message);

    const estimatedTokens = estimateTokens(session.messages);

    if (estimatedTokens > SUMMARY_TRIGGER) {
      await this.summarizeOldMessages(session);
    }

    this.sessions.set(sessionId, session);
  }

  private async summarizeOldMessages(
    session: ConversationSession
  ): Promise<void> {
    // Keep the last 10 messages verbatim, summarize the rest
    const toSummarize = session.messages.slice(0, -10);
    const recent      = session.messages.slice(-10);

    const summary = await summarizeConversation(toSummarize);

    session.messages = [
      {
        role:    'user',
        content: `[Previous conversation summary: ${summary}]`,
        id:      crypto.randomUUID(),
        timestamp: Date.now(),
      },
      ...recent,
    ];
  }
}

function estimateTokens(messages: ChatMessage[]): number {
  // Rule of thumb: ~1 token per 4 characters
  return messages.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0);
}
Working with Data in AI Pipelines

AI applications are data transformation machines — ingesting documents, parsing structured responses, normalizing schemas, and preparing data for embedding or generation.

Structured JSON Extraction from AI

typescript · structured data extraction
import { z } from 'zod';

// Define expected schema
const InvoiceSchema = z.object({
  invoiceNumber: z.string(),
  date:          z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  vendor:        z.object({
    name:    z.string(),
    email:   z.string().email().optional(),
    address: z.string().optional(),
  }),
  lineItems: z.array(z.object({
    description: z.string(),
    quantity:    z.number().positive(),
    unitPrice:   z.number().nonnegative(),
    total:       z.number().nonnegative(),
  })),
  subtotal:  z.number(),
  tax:       z.number().optional(),
  total:     z.number(),
  currency:  z.string().length(3).default('USD'),
});

type Invoice = z.infer<typeof InvoiceSchema>;

async function extractInvoice(
  client: Anthropic,
  documentText: string
): Promise<Invoice> {
  const response = await client.messages.create({
    model:      'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system:     'Extract invoice data. Respond with ONLY valid JSON, no markdown.',
    messages: [{
      role:    'user',
      content: `Extract invoice data from this document:\n\n${documentText}\n\nJSON schema: ${JSON.stringify(InvoiceSchema._def, null, 2)}`,
    }],
  });

  const raw = response.content[0].type === 'text'
    ? response.content[0].text.trim()
    : '';

  // Strip accidental markdown fences
  const cleaned = raw.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');

  // Parse and validate
  const parsed = JSON.parse(cleaned);
  return InvoiceSchema.parse(parsed); // throws ZodError if invalid
}

// Batch processing with error collection
async function batchExtractInvoices(
  client: Anthropic,
  documents: string[]
): Promise<{ success: Invoice[]; failed: { index: number; error: string }[] }> {
  const results = await Promise.allSettled(
    documents.map(doc => extractInvoice(client, doc))
  );

  return results.reduce(
    (acc, result, i) => {
      if (result.status === 'fulfilled') {
        acc.success.push(result.value);
      } else {
        acc.failed.push({ index: i, error: result.reason.message });
      }
      return acc;
    },
    { success: [] as Invoice[], failed: [] as { index: number; error: string }[] }
  );
}

Document Preprocessing for RAG

typescript · document chunking pipeline
interface DocumentChunk {
  id:         string;
  content:    string;
  metadata:   ChunkMetadata;
  embedding?: number[];
}

interface ChunkMetadata {
  sourceId:   string;
  chunkIndex: number;
  totalChunks: number;
  charStart:  number;
  charEnd:    number;
}

// Semantic chunking — split on section boundaries
function semanticChunk(
  text:     string,
  sourceId: string,
  options:  { maxChars: number; overlapChars: number } = {
    maxChars:     1500,
    overlapChars: 200,
  }
): DocumentChunk[] {
  const { maxChars, overlapChars } = options;

  // First try to split on headings (##, ###)
  let segments = text.split(/(?=\n#{1,3}\s)/);

  // Fall back to paragraph splits
  if (segments.every(s => s.length > maxChars)) {
    segments = text.split(/\n\n+/);
  }

  const chunks: DocumentChunk[] = [];
  let   buffer     = '';
  let   charOffset = 0;

  for (const segment of segments) {
    if (buffer.length + segment.length > maxChars && buffer.length > 0) {
      const charStart = charOffset - buffer.length;
      chunks.push({
        id:       `${sourceId}-${chunks.length}`,
        content:  buffer.trim(),
        metadata: {
          sourceId, chunkIndex: chunks.length,
          totalChunks: 0, // filled after
          charStart, charEnd: charOffset,
        },
      });
      // Overlap: keep last N chars for context continuity
      buffer = buffer.slice(-overlapChars);
    }
    buffer      += segment;
    charOffset  += segment.length;
  }

  if (buffer.trim()) {
    chunks.push({
      id:       `${sourceId}-${chunks.length}`,
      content:  buffer.trim(),
      metadata: {
        sourceId, chunkIndex: chunks.length,
        totalChunks: 0,
        charStart: charOffset - buffer.length,
        charEnd:   charOffset,
      },
    });
  }

  // Patch totalChunks
  return chunks.map(c => ({
    ...c,
    metadata: { ...c.metadata, totalChunks: chunks.length },
  }));
}
Scalable AI Application Architecture

Structure your project so that prompt definitions, model configuration, API integration, and UI components each have a clear home — and no layer bleeds into another.

Next.js Full-Stack AI App Structure

bash · project directory tree
my-ai-app/
├── app/                          # Next.js App Router
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   └── layout.tsx
│   ├── chat/
│   │   ├── [sessionId]/page.tsx
│   │   └── layout.tsx
│   ├── api/
│   │   ├── chat/route.ts         # Streaming chat endpoint
│   │   ├── tools/route.ts        # Tool execution endpoint
│   │   └── embed/route.ts        # Embedding endpoint
│   └── layout.tsx
│
├── lib/                          # Core application logic
│   ├── ai/
│   │   ├── client.ts             # Anthropic client singleton
│   │   ├── prompts/
│   │   │   ├── system.ts         # System prompt definitions
│   │   │   ├── tasks.ts          # Task-specific prompt templates
│   │   │   └── index.ts
│   │   ├── tools/
│   │   │   ├── definitions.ts    # Tool JSON schemas
│   │   │   ├── executor.ts       # Tool routing + execution
│   │   │   └── implementations/  # Actual tool functions
│   │   ├── context-manager.ts    # Context window management
│   │   └── streaming.ts          # SSE stream utilities
│   │
│   ├── db/
│   │   ├── client.ts             # Supabase / DB client
│   │   ├── queries/              # Typed query functions
│   │   └── schema.ts             # Zod schema ↔ DB types
│   │
│   ├── cache/
│   │   ├── redis.ts              # Redis client (Upstash)
│   │   └── strategies.ts         # Cache invalidation patterns
│   │
│   └── middleware/
│       ├── auth.ts               # JWT verification
│       ├── rate-limit.ts         # Per-user rate limiting
│       └── validate.ts           # Request validation
│
├── components/
│   ├── chat/
│   │   ├── ChatInterface.tsx     # Main chat component
│   │   ├── MessageBubble.tsx
│   │   ├── StreamingMarkdown.tsx
│   │   └── ToolCallDisplay.tsx
│   └── ui/                       # Design system components
│
├── hooks/
│   ├── useAIStream.ts
│   ├── useConversation.ts
│   └── useToolResults.ts
│
├── types/
│   ├── ai.ts                     # Core AI types
│   ├── api.ts                    # API request/response types
│   └── db.ts                     # Database entity types
│
├── config/
│   ├── models.ts                 # Model configurations
│   └── features.ts               # Feature flags
│
└── tests/
    ├── unit/                     # Jest unit tests
    ├── integration/              # API integration tests
    └── e2e/                      # Playwright E2E tests

Environment Configuration

typescript · typed env config
// config/env.ts — fail fast on missing env vars
import { z } from 'zod';

const EnvSchema = z.object({
  // AI
  ANTHROPIC_API_KEY:   z.string().min(1),
  OPENAI_API_KEY:      z.string().optional(),

  // Database
  DATABASE_URL:        z.string().url(),
  SUPABASE_ANON_KEY:   z.string().min(1),

  // Cache
  REDIS_URL:           z.string().url().optional(),

  // Auth
  JWT_SECRET:          z.string().min(32),
  NEXTAUTH_SECRET:     z.string().min(32),
  NEXTAUTH_URL:        z.string().url(),

  // App
  NODE_ENV:            z.enum(['development', 'production', 'test']),
  ALLOWED_ORIGINS:     z.string().optional(),
  MAX_REQUESTS_PER_MINUTE: z.coerce.number().default(20),
});

export const env = (() => {
  const result = EnvSchema.safeParse(process.env);
  if (!result.success) {
    console.error('❌ Invalid environment variables:');
    console.error(result.error.flatten().fieldErrors);
    process.exit(1); // Hard fail on startup
  }
  return result.data;
})();

// Types flow everywhere from this single source
export type Env = z.infer<typeof EnvSchema>;
Production-Grade AI Engineering

These practices separate hobby projects from production systems. Every AI application handling real users needs these foundations.

Security: Never Expose AI Keys to the Frontend

🔴
Critical security rule: AI API keys must NEVER be in frontend code, environment variables prefixed with NEXT_PUBLIC_, or client-side bundles. Always proxy through your backend.
typescript · input sanitization + prompt injection defense
// lib/security/sanitize.ts

// Patterns that indicate prompt injection attempts
const INJECTION_PATTERNS = [
  /ignore\s+(all\s+)?previous\s+instructions/i,
  /you\s+are\s+now\s+(a|an)\s+/i,
  /system\s+prompt\s*:/i,
  /\[INST\]/,
  /<<SYS>>/,
  /\[\/INST\]/,
];

export function sanitizeUserInput(input: string): {
  safe:    string;
  flagged: boolean;
  reason?: string;
} {
  // Length check
  if (input.length > 16_000) {
    return { safe: input.slice(0, 16_000), flagged: true, reason: 'Truncated: exceeded max length' };
  }

  // Injection detection
  for (const pattern of INJECTION_PATTERNS) {
    if (pattern.test(input)) {
      return { safe: '', flagged: true, reason: `Potential prompt injection: ${pattern}` };
    }
  }

  // Strip null bytes and control characters (keep \n, \t)
  const clean = input.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');

  return { safe: clean, flagged: false };
}

// Wrap user content to prevent bleeding into system context
export function wrapUserContent(content: string): string {
  return `<user_message>\n${content}\n</user_message>`;
}

// Rate limit by user + route
export const rateLimits = {
  chat:    { requests: 30, windowMs: 60_000 },  // 30/min
  embed:   { requests: 100, windowMs: 60_000 }, // 100/min
  analyze: { requests: 10,  windowMs: 60_000 }, // 10/min (expensive)
} as const;

Observability: Logging AI Interactions

typescript · structured AI logging
// lib/telemetry/ai-logger.ts
interface AICallLog {
  traceId:    string;
  userId:     string;
  model:      string;
  inputTokens:  number;
  outputTokens: number;
  latencyMs:  number;
  status:     'success' | 'error' | 'aborted';
  error?:     string;
  cost?:      number; // USD
  cached:     boolean;
}

const TOKEN_COSTS: Record<string, { input: number; output: number }> = {
  'claude-sonnet-4-20250514': { input: 0.000003, output: 0.000015 }, // per token
  'claude-opus-4-20250514':   { input: 0.000015, output: 0.000075 },
};

export async function logAICall(log: AICallLog): Promise<void> {
  const cost = TOKEN_COSTS[log.model];
  const totalCost = cost
    ? (log.inputTokens * cost.input + log.outputTokens * cost.output)
    : undefined;

  // Structured log for log aggregation (Datadog, Grafana, etc.)
  console.log(JSON.stringify({
    event:    'ai_call',
    level:    log.status === 'error' ? 'error' : 'info',
    ...log,
    cost:     totalCost?.toFixed(6),
    timestamp: new Date().toISOString(),
  }));

  // Async: persist to DB for cost tracking (fire and forget)
  persistToMetricsDB(log).catch(console.error);
}

Error Handling Hierarchy

RateLimitError
Retry with exponential backoff. Implement queue. Return 429 to client with Retry-After header.
ContextLengthError
Truncate conversation history. Trigger summarization. Log context size for monitoring.
NetworkTimeout
Retry 2-3x. Implement circuit breaker pattern. Fall back to secondary provider if available.
InvalidResponse
Log full response for debugging. Retry with lower temperature. Return structured error to frontend.
Production AI Application Patterns

Three complete application patterns you can adapt immediately. Each represents a class of AI product with distinct technical requirements.

Pattern 1 — AI Document Analyzer

📄
Use case: Upload a PDF contract/document → extract structured data, flag risks, generate summary. Core tech: file upload, base64 encoding, structured output extraction.
typescript · document analysis with vision
// api/analyze-document/route.ts
const DocumentAnalysisSchema = z.object({
  summary:   z.string(),
  keyPhrases: z.array(z.string()),
  risks: z.array(z.object({
    severity:    z.enum(['high', 'medium', 'low']),
    clause:      z.string(),
    explanation: z.string(),
  })),
  obligations: z.array(z.object({
    party:    z.string(),
    action:   z.string(),
    deadline: z.string().optional(),
  })),
  recommendation: z.enum(['sign', 'review', 'reject']),
});

export async function analyzeDocument(
  client: Anthropic,
  pdfBase64: string
): Promise<z.infer<typeof DocumentAnalysisSchema>> {
  const response = await client.messages.create({
    model:      'claude-sonnet-4-20250514',
    max_tokens: 4096,
    system: `You are a contract analysis AI.
Analyze the provided document and respond with ONLY valid JSON.
Focus on: risks, obligations, and whether to sign.`,
    messages: [{
      role: 'user',
      content: [
        {
          type:   'document',
          source: { type: 'base64', media_type: 'application/pdf', data: pdfBase64 },
        },
        {
          type: 'text',
          text: 'Analyze this contract. Return JSON with: summary, keyPhrases, risks[], obligations[], recommendation.',
        },
      ],
    }],
  });

  const raw     = (response.content[0] as { text: string }).text;
  const cleaned = raw.replace(/^```json?\n?/, '').replace(/\n?```$/, '');
  return DocumentAnalysisSchema.parse(JSON.parse(cleaned));
}

Pattern 2 — Multi-Turn AI Chat with Memory

typescript · persistent conversation agent
// A stateful agent that remembers user preferences across sessions
class PersonalizedAgent {
  private client:   Anthropic;
  private db:       Database;
  private memory:   ContextManager;

  constructor(client: Anthropic, db: Database) {
    this.client = client;
    this.db     = db;
    this.memory = new ContextManager();
  }

  async chat(
    userId:    string,
    sessionId: string,
    userInput: string
  ): Promise<AsyncIterable<string>> {
    // 1. Load user profile and preferences
    const profile = await this.db.users.get(userId);

    // 2. Load conversation history
    const history = await this.memory.getContext(sessionId);

    // 3. Build personalized system prompt
    const system = `You are a helpful assistant.
User profile: ${JSON.stringify(profile.preferences)}
Communication style: ${profile.tone ?? 'professional'}
Language: ${profile.preferredLanguage ?? 'English'}
Always address them as ${profile.displayName}.`;

    // 4. Persist user message
    await this.memory.addMessage(sessionId, {
      id:        crypto.randomUUID(),
      role:      'user',
      content:   userInput,
      timestamp: Date.now(),
    });

    // 5. Stream response
    const stream = this.client.messages.stream({
      model:      'claude-sonnet-4-20250514',
      max_tokens: 2048,
      system,
      messages:   [...history, { role: 'user', content: userInput }],
    });

    // 6. Collect and persist assistant reply
    const generator = async function* (this: PersonalizedAgent) {
      let fullReply = '';
      for await (const event of stream) {
        if (event.type === 'content_block_delta' &&
            event.delta.type === 'text_delta') {
          fullReply += event.delta.text;
          yield event.delta.text;
        }
      }
      await this.memory.addMessage(sessionId, {
        id:        crypto.randomUUID(),
        role:      'assistant',
        content:   fullReply,
        timestamp: Date.now(),
      });
    }.bind(this);

    return generator();
  }
}

Pattern 3 — AI Content Pipeline (Batch)

typescript · batch content generation pipeline
// Use Anthropic's Batch API for high-volume, non-real-time tasks
import Anthropic from '@anthropic-ai/sdk';

async function batchGenerateDescriptions(
  client: Anthropic,
  products: Array<{ id: string; name: string; specs: string }>
): Promise<Map<string, string>> {
  // Create batch requests
  const requests: Anthropic.MessageCreateParamsNonStreaming[] = products.map(p => ({
    custom_id: p.id,
    params: {
      model:      'claude-haiku-4-5-20251001', // fast + cheap for bulk
      max_tokens: 300,
      messages: [{
        role:    'user',
        content: `Write a compelling 2-sentence product description for:\nName: ${p.name}\nSpecs: ${p.specs}`,
      }],
    },
  }));

  // Submit batch (async, no streaming needed)
  const batch = await client.batches.create({ requests });
  console.log(`Batch ${batch.id} submitted: ${batch.request_counts.processing} requests`);

  // Poll until complete
  let status = batch;
  while (status.processing_status !== 'ended') {
    await delay(5_000);
    status = await client.batches.retrieve(batch.id);
    console.log(`Status: ${status.processing_status} | Done: ${status.request_counts.succeeded}`);
  }

  // Collect results
  const results = new Map<string, string>();
  for await (const result of await client.batches.results(batch.id)) {
    if (result.result.type === 'succeeded') {
      const text = result.result.message.content[0];
      if (text.type === 'text') {
        results.set(result.custom_id, text.text);
      }
    }
  }

  console.log(`✅ Batch complete: ${results.size}/${products.length} succeeded`);
  return results;
}
Scaling AI Applications in Production

AI API calls are expensive — in both latency and cost. These patterns help you serve more users with lower spend and better response times.

Response Caching Strategy

typescript · semantic cache layer
import { Redis } from '@upstash/redis';

const redis = Redis.fromEnv();

// Deterministic cache key: model + hash of inputs
function cacheKey(
  model:    string,
  system:   string,
  messages: ChatMessage[]
): string {
  const payload = JSON.stringify({ model, system, messages });
  // Simple hash (use crypto.subtle for production)
  const hash = [...payload].reduce((acc, c) => (acc * 31 + c.charCodeAt(0)) | 0, 0);
  return `ai:cache:${Math.abs(hash).toString(36)}`;
}

async function cachedCompletion(
  client:   Anthropic,
  model:    string,
  system:   string,
  messages: ChatMessage[],
  ttlSecs = 3600 // 1 hour default
): Promise<string> {
  // Only cache deterministic (temperature=0) requests
  const key    = cacheKey(model, system, messages);
  const cached = await redis.get<string>(key);

  if (cached) {
    console.log(`Cache HIT: ${key}`);
    return cached;
  }

  const response = await client.messages.create({
    model, max_tokens: 2048, system, messages,
    temperature: 0, // deterministic for cacheability
  });

  const text = response.content[0].type === 'text'
    ? response.content[0].text
    : '';

  await redis.setex(key, ttlSecs, text);
  return text;
}

// Prompt caching — reduce input token costs 90% on repeated system prompts
const clientWithPromptCaching = new Anthropic();

async function cachedSystemPrompt(
  userMessage: string,
  largeSystemPrompt: string  // documents, instructions, etc.
): Promise<string> {
  const response = await clientWithPromptCaching.messages.create({
    model:      'claude-sonnet-4-20250514',
    max_tokens: 1024,
    system: [{
      type: 'text',
      text: largeSystemPrompt,
      cache_control: { type: 'ephemeral' }, // ← Cache this prefix
    }],
    messages: [{ role: 'user', content: userMessage }],
  });
  return (response.content[0] as { text: string }).text;
}

Performance Summary

Technique Latency Impact Cost Impact Complexity
Response caching↓ 99% (cache hit)↓ 99%Low
Prompt caching↓ 85% (input)↓ 90% on inputLow
Parallel tool calls↓ N×—Medium
Batch API+hours (async)↓ 50%Medium
Model routing↓ 60% (haiku)↓ 90%Medium
Edge deployment↓ 30–50ms—High
Streaming↓ TTFB 80%—Low

Model Routing by Complexity

typescript · intelligent model router
type ModelTier = 'fast' | 'balanced' | 'powerful';

interface RoutingCriteria {
  estimatedComplexity: 'simple' | 'medium' | 'complex';
  requiresReasoning:   boolean;
  contextLength:       number;
  latencySensitive:    boolean;
}

const MODEL_MAP: Record<ModelTier, string> = {
  fast:     'claude-haiku-4-5-20251001',     // Simple tasks, high volume
  balanced: 'claude-sonnet-4-20250514',      // Most production use cases
  powerful: 'claude-opus-4-20250514',        // Complex reasoning, rare
};

function routeModel(criteria: RoutingCriteria): string {
  const { estimatedComplexity, requiresReasoning, contextLength, latencySensitive } = criteria;

  // Long context + deep reasoning → powerful
  if (contextLength > 100_000 || requiresReasoning && estimatedComplexity === 'complex') {
    return MODEL_MAP.powerful;
  }

  // Real-time simple tasks → fast
  if (latencySensitive && estimatedComplexity === 'simple') {
    return MODEL_MAP.fast;
  }

  // Default to balanced
  return MODEL_MAP.balanced;
}

// Auto-estimate complexity from input
function estimateComplexity(messages: ChatMessage[]): RoutingCriteria {
  const lastMessage = messages.at(-1)?.content ?? '';
  const totalChars  = messages.reduce((s, m) => s + m.content.length, 0);

  const COMPLEX_SIGNALS = [
    'analyze', 'reason', 'compare', 'evaluate', 'strategy',
    'architecture', 'design', 'review', 'audit',
  ];

  const isComplex = COMPLEX_SIGNALS.some(s =>
    lastMessage.toLowerCase().includes(s)
  );

  return {
    estimatedComplexity: isComplex ? 'complex' : totalChars > 2000 ? 'medium' : 'simple',
    requiresReasoning:   isComplex,
    contextLength:       totalChars,
    latencySensitive:    messages.length < 3, // first few messages need to be fast
  };
}
🚀
You've reached the end of this guide. The patterns here are production-tested and continuously updated. Build something — the best way to internalize these patterns is to apply them to a real project. Start with Section 12's patterns and adapt from there.
Continue Learning
Explore more courses
Next.js + AI Vector Databases & RAG Agentic Systems AI for Indie Founders Edge AI Deployment LLM Fine-Tuning

Was this guide helpful?

Your feedback helps us continuously improve our developer docs.

Explore More Docs →