Skip to content
All essays
AIMarch 10, 202525 min

Building AI-Powered Web Applications

Learn integration patterns, API design, and best practices for building AI-powered web applications in 2025.

Ü
Ümit Uz
Mobile & Full Stack Developer

The landscape of web development is being transformed by artificial intelligence. As we move through 2025, integrating AI capabilities into web applications has become not just a competitive advantage, but a necessity. This guide explores modern integration patterns, API design principles, and practical implementation strategies for building AI-powered web applications.

Understanding AI Integration Architecture

Before diving into implementation, it's crucial to understand the architecture patterns that make AI integration successful and scalable.

Direct Integration Pattern

The simplest approach involves direct API calls to AI services from your frontend or backend:

typescript
// Direct OpenAI API integration
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

async function generateContent(prompt: string) {
  const completion = await openai.chat.completions.create({
    model: 'gpt-4-turbo',
    messages: [{ role: 'user', content: prompt }],
    temperature: 0.7,
    max_tokens: 1000
  });

  return completion.choices[0].message.content;
}

Pros: Simple to implement, low latency Cons: Tightly coupled, difficult to swap providers, API keys exposure risk

Create a backend proxy that handles AI API calls:

typescript
// Backend API route (Next.js example)
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

export async function POST(request: NextRequest) {
  try {
    const { prompt, options = {} } = await request.json();

    const completion = await openai.chat.completions.create({
      model: options.model || 'gpt-4-turbo',
      messages: [
        {
          role: 'system',
          content: 'You are a helpful assistant specialized in technical content.'
        },
        { role: 'user', content: prompt }
      ],
      temperature: options.temperature ?? 0.7,
      max_tokens: options.maxTokens ?? 1000,
      stream: options.stream ?? false
    });

    return NextResponse.json({
      content: completion.choices[0].message.content,
      usage: completion.usage
    });
  } catch (error) {
    console.error('AI API Error:', error);
    return NextResponse.json(
      { error: 'Failed to generate content' },
      { status: 500 }
    );
  }
}

Frontend consumption:

typescript
// Frontend service
interface AIResponse {
  content: string;
  usage: {
    prompt_tokens: number;
    completion_tokens: number;
    total_tokens: number;
  };
}

class AIService {
  private apiBase: string;

  constructor(apiBase: string = '/api/ai') {
    this.apiBase = apiBase;
  }

  async generateText(
    prompt: string,
    options?: {
      model?: string;
      temperature?: number;
      maxTokens?: number;
    }
  ): Promise<AIResponse> {
    const response = await fetch(this.apiBase, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt, options })
    });

    if (!response.ok) {
      throw new Error(`AI service error: ${response.statusText}`);
    }

    return response.json();
  }

  async streamText(
    prompt: string,
    onChunk: (chunk: string) => void,
    options?: {
      model?: string;
      temperature?: number;
    }
  ): Promise<void> {
    const response = await fetch(this.apiBase, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt, options: { ...options, stream: true } })
    });

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

    if (!reader) throw new Error('No response body');

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

      const chunk = decoder.decode(value);
      onChunk(chunk);
    }
  }
}

// Usage example
const aiService = new AIService();

// Non-streaming
const result = await aiService.generateText(
  'Explain quantum computing in simple terms',
  { temperature: 0.8, maxTokens: 500 }
);

// Streaming
await aiService.streamText(
  'Write a short story about AI',
  (chunk) => console.log(chunk),
  { temperature: 0.9 }
);

Multi-Provider Abstraction Layer

Build flexibility into your application by creating an abstraction layer:

typescript
// AI Provider Interface
interface AIProvider {
  generateText(prompt: string, options?: GenerationOptions): Promise<string>;
  streamText(
    prompt: string,
    onChunk: (chunk: string) => void,
    options?: GenerationOptions
  ): Promise<void>;
  embedText(text: string): Promise<number[]>;
}

interface GenerationOptions {
  temperature?: number;
  maxTokens?: number;
  model?: string;
  topP?: number;
  frequencyPenalty?: number;
  presencePenalty?: number;
}

// OpenAI Implementation
class OpenAIProvider implements AIProvider {
  private client: OpenAI;

  constructor(apiKey: string) {
    this.client = new OpenAI({ apiKey });
  }

  async generateText(prompt: string, options?: GenerationOptions): Promise<string> {
    const completion = await this.client.chat.completions.create({
      model: options?.model || 'gpt-4-turbo',
      messages: [{ role: 'user', content: prompt }],
      temperature: options?.temperature ?? 0.7,
      max_tokens: options?.maxTokens ?? 1000
    });

    return completion.choices[0].message.content || '';
  }

  async streamText(
    prompt: string,
    onChunk: (chunk: string) => void,
    options?: GenerationOptions
  ): Promise<void> {
    const stream = await this.client.chat.completions.create({
      model: options?.model || 'gpt-4-turbo',
      messages: [{ role: 'user', content: prompt }],
      temperature: options?.temperature ?? 0.7,
      stream: true
    });

    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) onChunk(content);
    }
  }

  async embedText(text: string): Promise<number[]> {
    const response = await this.client.embeddings.create({
      model: 'text-embedding-3-small',
      input: text
    });

    return response.data[0].embedding;
  }
}

// Anthropic Implementation
class AnthropicProvider implements AIProvider {
  private client: Anthropic;

  constructor(apiKey: string) {
    this.client = new Anthropic({ apiKey });
  }

  async generateText(prompt: string, options?: GenerationOptions): Promise<string> {
    const message = await this.client.messages.create({
      model: options?.model || 'claude-3-opus-20240229',
      max_tokens: options?.maxTokens ?? 1000,
      messages: [{ role: 'user', content: prompt }]
    });

    return message.content[0].type === 'text' ? message.content[0].text : '';
  }

  async streamText(
    prompt: string,
    onChunk: (chunk: string) => void,
    options?: GenerationOptions
  ): Promise<void> {
    const stream = await this.client.messages.create({
      model: options?.model || 'claude-3-opus-20240229',
      max_tokens: options?.maxTokens ?? 1000,
      messages: [{ role: 'user', content: prompt }],
      stream: true
    });

    for await (const chunk of stream) {
      if (chunk.type === 'content_block_delta') {
        onChunk(chunk.delta.text);
      }
    }
  }

  async embedText(text: string): Promise<number[]> {
    // Implementation for embeddings
    throw new Error('Embeddings not implemented for Anthropic');
  }
}

// Factory pattern
class AIProviderFactory {
  static create(provider: 'openai' | 'anthropic', apiKey: string): AIProvider {
    switch (provider) {
      case 'openai':
        return new OpenAIProvider(apiKey);
      case 'anthropic':
        return new AnthropicProvider(apiKey);
      default:
        throw new Error(`Unknown provider: ${provider}`);
    }
  }
}

// Usage
const ai = AIProviderFactory.create(
  process.env.NEXT_PUBLIC_AI_PROVIDER as any,
  process.env.AI_API_KEY!
);

const response = await ai.generateText('Hello, AI!');

API Design Best Practices

1. Structured Request/Response Format

Design clear, type-safe interfaces:

typescript
// Request types
interface AIRequest {
  prompt: string;
  context?: string[];
  options?: {
    temperature?: number;
    maxTokens?: number;
    model?: string;
    stream?: boolean;
  };
  metadata?: {
    userId?: string;
    sessionId?: string;
    requestId?: string;
  };
}

// Response types
interface AIResponse {
  id: string;
  content: string;
  model: string;
  usage: {
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
  };
  latency: number;
  timestamp: string;
}

// Error response
interface AIErrorResponse {
  error: {
    code: string;
    message: string;
    details?: any;
  };
  requestId: string;
}

2. Rate Limiting and Quota Management

Implement robust rate limiting:

typescript
// Rate limiter using Redis or in-memory
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

// Create rate limiter
const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
  analytics: true
});

// Middleware
export async function rateLimitMiddleware(
  request: NextRequest,
  identifier: string
) {
  const { success, limit, reset, remaining } = await ratelimit.limit(identifier);

  if (!success) {
    return NextResponse.json(
      {
        error: 'Rate limit exceeded',
        limit,
        reset: new Date(reset).toISOString()
      },
      { status: 429, headers: { 'Retry-After': '60' } }
    );
  }

  return null; // No error
}

// API route with rate limiting
export async function POST(request: NextRequest) {
  const userId = request.headers.get('x-user-id') || 'anonymous';

  const rateLimitError = await rateLimitMiddleware(request, userId);
  if (rateLimitError) return rateLimitError;

  // Process request...
}

3. Caching Strategy

Implement intelligent caching to reduce costs and latency:

typescript
// Cache configuration
interface CacheConfig {
  enabled: boolean;
  ttl: number; // Time to live in seconds
  maxSize: number;
}

// Semantic cache (using embeddings)
class SemanticCache {
  private cache: Map<string, { content: string; timestamp: number }>;
  private embeddings: Map<string, number[]>;
  private config: CacheConfig;

  constructor(config: CacheConfig) {
    this.cache = new Map();
    this.embeddings = new Map();
    this.config = config;
  }

  private getCacheKey(prompt: string): string {
    // Create hash of prompt
    return crypto.createHash('sha256').update(prompt).digest('hex');
  }

  private async similarity(embedding1: number[], embedding2: number[]): Promise<number> {
    // Cosine similarity
    const dotProduct = embedding1.reduce((sum, a, i) => sum + a * embedding2[i], 0);
    const magnitude1 = Math.sqrt(embedding1.reduce((sum, a) => sum + a * a, 0));
    const magnitude2 = Math.sqrt(embedding2.reduce((sum, a) => sum + a * a, 0));
    return dotProduct / (magnitude1 * magnitude2);
  }

  async get(prompt: string, embedding: number[]): Promise<string | null> {
    if (!this.config.enabled) return null;

    const cacheKey = this.getCacheKey(prompt);
    const cached = this.cache.get(cacheKey);

    if (cached) {
      const age = (Date.now() - cached.timestamp) / 1000;
      if (age < this.config.ttl) {
        return cached.content;
      }
    }

    // Semantic similarity search
    for (const [key, value] of this.cache.entries()) {
      const cachedEmbedding = this.embeddings.get(key);
      if (cachedEmbedding) {
        const sim = await this.similarity(embedding, cachedEmbedding);
        if (sim > 0.95) { // 95% similarity threshold
          const age = (Date.now() - value.timestamp) / 1000;
          if (age < this.config.ttl) {
            return value.content;
          }
        }
      }
    }

    return null;
  }

  async set(prompt: string, content: string, embedding: number[]): Promise<void> {
    if (!this.config.enabled) return;

    // Evict oldest if at capacity
    if (this.cache.size >= this.config.maxSize) {
      const oldestKey = [...this.cache.entries()]
        .sort((a, b) => a[1].timestamp - b[1].timestamp)[0][0];
      this.cache.delete(oldestKey);
      this.embeddings.delete(oldestKey);
    }

    const cacheKey = this.getCacheKey(prompt);
    this.cache.set(cacheKey, {
      content,
      timestamp: Date.now()
    });
    this.embeddings.set(cacheKey, embedding);
  }
}

Real-World Implementation Examples

AI-Powered Code Assistant

typescript
// Code explanation service
class CodeAssistant {
  private ai: AIProvider;
  private cache: SemanticCache;

  constructor(ai: AIProvider) {
    this.ai = ai;
    this.cache = new SemanticCache({
      enabled: true,
      ttl: 3600,
      maxSize: 1000
    });
  }

  async explainCode(code: string, language: string): Promise<string> {
    const prompt = `Explain this ${language} code:

${code}


Provide:
1. Brief overview
2. Key functionality
3. Potential issues
4. Suggestions for improvement`;

    // Check cache
    const embedding = await this.ai.embedText(prompt);
    const cached = await this.cache.get(prompt, embedding);
    if (cached) return cached;

    // Generate explanation
    const explanation = await this.ai.generateText(prompt, {
      temperature: 0.3,
      maxTokens: 1500
    });

    // Cache result
    await this.cache.set(prompt, explanation, embedding);

    return explanation;
  }

  async generateTests(code: string, language: string): Promise<string> {
    const prompt = `Generate comprehensive unit tests for this ${language} code:

${code}


Include:
1. Happy path tests
2. Edge cases
3. Error handling
4. Mock setup if needed`;

    return this.ai.generateText(prompt, {
      temperature: 0.4,
      maxTokens: 2000
    });
  }

  async refactorCode(code: string, language: string): Promise<{
    refactored: string;
    explanation: string;
  }> {
    const prompt = `Refactor this ${language} code to improve:
- Readability
- Performance
- Error handling
- Best practices

Original code:

${code}


Provide the refactored code and explain the changes.`;

    const response = await this.ai.generateText(prompt, {
      temperature: 0.2,
      maxTokens: 2500
    });

    // Parse response to extract code and explanation
    const codeMatch = response.match(/\`\`\`${language}\n([\s\S]*?)\`\`\`/);
    const refactored = codeMatch ? codeMatch[1] : code;
    const explanation = response.replace(/\`\`\${language}[\s\S]*?\`\`\`/, '').trim();

    return { refactored, explanation };
  }
}

// React component integration
import { useState } from 'react';

export function CodeAssistantComponent() {
  const [code, setCode] = useState('');
  const [language, setLanguage] = useState('javascript');
  const [explanation, setExplanation] = useState('');
  const [loading, setLoading] = useState(false);

  const assistant = new CodeAssistant(ai);

  const handleExplain = async () => {
    setLoading(true);
    try {
      const result = await assistant.explainCode(code, language);
      setExplanation(result);
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="p-6 max-w-4xl mx-auto">
      <h2 className="text-2xl font-bold mb-4">AI Code Assistant</h2>

      <textarea
        value={code}
        onChange={(e) => setCode(e.target.value)}
        placeholder="Paste your code here..."
        className="w-full h-64 p-4 border rounded font-mono text-sm"
      />

      <select
        value={language}
        onChange={(e) => setLanguage(e.target.value)}
        className="mt-4 p-2 border rounded"
      >
        <option value="javascript">JavaScript</option>
        <option value="python">Python</option>
        <option value="typescript">TypeScript</option>
        <option value="java">Java</option>
      </select>

      <button
        onClick={handleExplain}
        disabled={loading || !code}
        className="mt-4 px-6 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {loading ? 'Analyzing...' : 'Explain Code'}
      </button>

      {explanation && (
        <div className="mt-6 p-4 bg-gray-50 rounded">
          <h3 className="font-bold mb-2">Explanation:</h3>
          <div className="prose prose-sm">{explanation}</div>
        </div>
      )}
    </div>
  );
}

Intelligent Search with AI

typescript
// Semantic search service
class SemanticSearch {
  private ai: AIProvider;
  private documents: Map<string, { content: string; embedding: number[]; metadata: any }>;

  constructor(ai: AIProvider) {
    this.ai = ai;
    this.documents = new Map();
  }

  async indexDocument(
    id: string,
    content: string,
    metadata: any = {}
  ): Promise<void> {
    const embedding = await this.ai.embedText(content);
    this.documents.set(id, { content, embedding, metadata });
  }

  private cosineSimilarity(a: number[], b: number[]): number {
    const dotProduct = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
    const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0));
    const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0));
    return dotProduct / (magnitudeA * magnitudeB);
  }

  async search(query: string, topK: number = 5): Promise<Array<{
    id: string;
    content: string;
    similarity: number;
    metadata: any;
  }>> {
    const queryEmbedding = await this.ai.embedText(query);

    const results = Array.from(this.documents.entries())
      .map(([id, doc]) => ({
        id,
        content: doc.content,
        similarity: this.cosineSimilarity(queryEmbedding, doc.embedding),
        metadata: doc.metadata
      }))
      .filter(result => result.similarity > 0.7) // Similarity threshold
      .sort((a, b) => b.similarity - a.similarity)
      .slice(0, topK);

    return results;
  }

  async searchWithRerank(
    query: string,
    topK: number = 10
  ): Promise<Array<{
    id: string;
    content: string;
    score: number;
    metadata: any;
  }>> {
    // Initial retrieval
    const candidates = await this.search(query, topK * 2);

    // Rerank using AI
    const prompt = `Rank these documents by relevance to the query: "${query}"

Documents:
${candidates.map((doc, i) => `${i + 1}. ${doc.content}`).join('\n')}

Return only the numbers of the top ${topK} most relevant documents, in order.`;

    const response = await this.ai.generateText(prompt, { temperature: 0.1 });

    // Parse response and return reranked results
    const rankings = response.match(/\d+/g)?.map(Number) || [];
    return rankings
      .slice(0, topK)
      .map(rank => candidates[rank - 1])
      .filter(Boolean);
  }
}

// Usage
const search = new SemanticSearch(ai);

// Index documents
await search.indexDocument('doc1', 'Machine learning is a subset of AI', { category: 'ai' });
await search.indexDocument('doc2', 'React is a JavaScript library for building UIs', { category: 'web' });
await search.indexDocument('doc3', 'Python is widely used in data science', { category: 'data' });

// Search
const results = await search.search('artificial intelligence and data');
console.log(results);

Performance Optimization

1. Streaming Responses

Implement streaming for better UX:

typescript
// Streaming hook for React
import { useState, useCallback } from 'react';

export function useAIStream() {
  const [content, setContent] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const stream = useCallback(async (prompt: string) => {
    setLoading(true);
    setError(null);
    setContent('');

    try {
      const response = await fetch('/api/ai/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt })
      });

      if (!response.ok) throw new Error('Stream failed');

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

      if (!reader) throw new Error('No reader');

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

        const chunk = decoder.decode(value);
        setContent(prev => prev + chunk);
      }
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  }, []);

  return { content, loading, error, stream };
}

// Usage in component
function StreamingComponent() {
  const { content, loading, stream } = useAIStream();

  return (
    <div>
      <button onClick={() => stream('Tell me a story')}>
        {loading ? 'Streaming...' : 'Generate'}
      </button>
      {content && <div className="mt-4">{content}</div>}
    </div>
  );
}

2. Batch Processing

typescript
// Batch processing for multiple requests
class BatchProcessor {
  private queue: Array<{
    prompt: string;
    resolve: (result: string) => void;
    reject: (error: Error) => void;
  }> = [];
  private processing = false;
  private batchSize = 5;
  private batchDelay = 100; // ms

  async process(prompt: string): Promise<string> {
    return new Promise((resolve, reject) => {
      this.queue.push({ prompt, resolve, reject });
      this.processBatch();
    });
  }

  private async processBatch() {
    if (this.processing || this.queue.length < this.batchSize) {
      return;
    }

    this.processing = true;
    const batch = this.queue.splice(0, this.batchSize);

    try {
      // Process batch (example with OpenAI batch API)
      const results = await Promise.all(
        batch.map(item => this.ai.generateText(item.prompt))
      );

      batch.forEach((item, i) => item.resolve(results[i]));
    } catch (error) {
      batch.forEach(item => item.reject(error as Error));
    } finally {
      this.processing = false;

      // Check if more items to process
      if (this.queue.length > 0) {
        setTimeout(() => this.processBatch(), this.batchDelay);
      }
    }
  }
}

Monitoring and Analytics

Track AI Usage

typescript
// Analytics service
class AIAnalytics {
  private events: Array<{
    timestamp: number;
    event: string;
    data: any;
  }> = [];

  trackRequest(data: {
    model: string;
    promptTokens: number;
    completionTokens: number;
    latency: number;
    success: boolean;
  }) {
    this.events.push({
      timestamp: Date.now(),
      event: 'ai_request',
      data
    });

    // Send to analytics service
    this.flush();
  }

  trackError(error: {
    code: string;
    message: string;
    provider: string;
  }) {
    this.events.push({
      timestamp: Date.now(),
      event: 'ai_error',
      data: error
    });
  }

  private flush() {
    // Send to your analytics backend
    fetch('/api/analytics', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ events: this.events })
    }).catch(console.error);

    this.events = [];
  }

  getStats(timeRange: number = 3600000) {
    // Calculate statistics for dashboard
    const now = Date.now();
    const recent = this.events.filter(
      e => now - e.timestamp < timeRange
    );

    return {
      totalRequests: recent.filter(e => e.event === 'ai_request').length,
      errors: recent.filter(e => e.event === 'ai_error').length,
      avgLatency: recent
        .filter(e => e.event === 'ai_request')
        .reduce((sum, e) => sum + e.data.latency, 0) / recent.length || 0,
      totalTokens: recent
        .filter(e => e.event === 'ai_request')
        .reduce((sum, e) => sum + e.data.promptTokens + e.data.completionTokens, 0)
    };
  }
}

Security Best Practices

1. API Key Management

typescript
// Never expose API keys on frontend
// Use environment variables on server-side
// Rotate keys regularly
// Use separate keys for development/production

// Server-side key management
class APIKeyManager {
  private keys: Map<string, { key: string; usage: number; limit: number }>;

  constructor() {
    this.keys = new Map();
    this.initializeKeys();
  }

  private initializeKeys() {
    // Load from environment
    const keys = process.env.AI_API_KEYS?.split(',') || [];
    keys.forEach(key => {
      this.keys.set(key, { key, usage: 0, limit: 100000 });
    });
  }

  getKey(): string {
    // Round-robin or least-used key selection
    const entries = Array.from(this.keys.values());
    const selected = entries.sort((a, b) => a.usage - b.usage)[0];

    if (selected.usage >= selected.limit) {
      throw new Error('All API keys have reached their limits');
    }

    selected.usage++;
    return selected.key;
  }
}

2. Input Sanitization

typescript
// Sanitize user input before sending to AI
function sanitizeInput(input: string): string {
  return input
    .trim()
    .replace(/<script[^>]*>.*?<\/script>/gi, '') // Remove scripts
    .slice(0, 10000); // Limit length
}

// Validate output
function sanitizeOutput(output: string): string {
  // Remove any potentially harmful content
  return output
    .replace(/<script[^>]*>.*?<\/script>/gi, '')
    .replace(/javascript:/gi, '')
    .replace(/on\w+\s*=/gi, '');
}

Conclusion

Building AI-powered web applications in 2025 requires careful consideration of architecture, API design, performance, security, and user experience. By following the patterns and best practices outlined in this guide, you can create scalable, maintainable, and powerful AI-integrated applications.

Key takeaways:

  1. 1Use a proxy pattern for API calls
  2. 2Implement multi-provider abstraction
  3. 3Add caching and rate limiting
  4. 4Monitor usage and performance
  5. 5Prioritize security and input validation
  6. 6Design for streaming and real-time experiences

The AI landscape continues to evolve rapidly, but these fundamental patterns will serve as a solid foundation for your AI-powered web applications.

Related essays

Next essay
Web3 Development with Ethers.js: Building Production DApps