Documentation

API Integration

Connect Angular AI Kit to OpenAI, Anthropic, and other AI providers.

Overview

Architecture for AI API integration

Important: Never expose API keys in your Angular frontend. Always use a backend proxy to make API calls securely.

Recommended Architecture

Angular App  →  Your Backend  →  AI Provider API
  (fetch)        (proxy)          (OpenAI, etc.)
        
  • Frontend makes requests to your backend
  • Backend adds API key and forwards to AI provider
  • Backend streams response back to frontend
  • This keeps your API keys secure

OpenAI Integration

Stream responses from GPT-4 and other OpenAI models

Frontend Service

1// openai.service.ts2import { Injectable, inject, signal } from '@angular/core';3import { HttpClient } from '@angular/common/http';4 5@Injectable({ providedIn: 'root' })6export class OpenAIService {7  private http = inject(HttpClient);8  private apiUrl = '/api/chat'; // Your backend proxy9 10  private streamingContent = signal('');11  readonly streamingContent$ = this.streamingContent.asReadonly();12 13  async streamChat(messages: { role: string; content: string }[]): Promise<string> {14    this.streamingContent.set('');15 16    const response = await fetch(this.apiUrl, {17      method: 'POST',18      headers: {19        'Content-Type': 'application/json',20      },21      body: JSON.stringify({22        model: 'gpt-4',23        messages,24        stream: true,25      }),26    });27 28    if (!response.ok) {29      throw new Error(`API error: ${response.status}`);30    }31 32    const reader = response.body?.getReader();33    const decoder = new TextDecoder();34    let fullContent = '';35 36    if (!reader) {37      throw new Error('No response body');38    }39 40    while (true) {41      const { done, value } = await reader.read();42      if (done) break;43 44      const chunk = decoder.decode(value);45      const lines = chunk.split('\n');46 47      for (const line of lines) {48        if (line.startsWith('data: ')) {49          const data = line.slice(6);50          if (data === '[DONE]') continue;51 52          try {53            const parsed = JSON.parse(data);54            const content = parsed.choices?.[0]?.delta?.content || '';55            fullContent += content;56            this.streamingContent.set(fullContent);57          } catch {58            // Skip invalid JSON59          }60        }61      }62    }63 64    this.streamingContent.set('');65    return fullContent;66  }67}

Backend Proxy (Node.js)

1// server.ts (Node.js/Express example)2import express from 'express';3import OpenAI from 'openai';4 5const app = express();6app.use(express.json());7 8const openai = new OpenAI({9  apiKey: process.env.OPENAI_API_KEY,10});11 12app.post('/api/chat', async (req, res) => {13  const { messages, model = 'gpt-4', stream = true } = req.body;14 15  try {16    if (stream) {17      res.setHeader('Content-Type', 'text/event-stream');18      res.setHeader('Cache-Control', 'no-cache');19      res.setHeader('Connection', 'keep-alive');20 21      const completion = await openai.chat.completions.create({22        model,23        messages,24        stream: true,25      });26 27      for await (const chunk of completion) {28        const content = chunk.choices[0]?.delta?.content || '';29        if (content) {30          res.write(`data: ${JSON.stringify(chunk)}\n\n`);31        }32      }33 34      res.write('data: [DONE]\n\n');35      res.end();36    } else {37      const completion = await openai.chat.completions.create({38        model,39        messages,40      });41 42      res.json(completion);43    }44  } catch (error) {45    console.error('OpenAI API error:', error);46    res.status(500).json({ error: 'Failed to get response' });47  }48});49 50app.listen(3000, () => console.log('Server running on port 3000'));

Anthropic (Claude) Integration

Stream responses from Claude models

Anthropic's API uses a slightly different streaming format:

1// anthropic.service.ts2import { Injectable, signal } from '@angular/core';3 4@Injectable({ providedIn: 'root' })5export class AnthropicService {6  private apiUrl = '/api/anthropic';7  private streamingContent = signal('');8  readonly streamingContent$ = this.streamingContent.asReadonly();9 10  async streamChat(messages: { role: string; content: string }[]): Promise<string> {11    this.streamingContent.set('');12 13    const response = await fetch(this.apiUrl, {14      method: 'POST',15      headers: { 'Content-Type': 'application/json' },16      body: JSON.stringify({17        model: 'claude-3-opus-20240229',18        messages,19        max_tokens: 4096,20        stream: true,21      }),22    });23 24    const reader = response.body?.getReader();25    const decoder = new TextDecoder();26    let fullContent = '';27 28    if (!reader) throw new Error('No response body');29 30    while (true) {31      const { done, value } = await reader.read();32      if (done) break;33 34      const chunk = decoder.decode(value);35      const lines = chunk.split('\n');36 37      for (const line of lines) {38        if (line.startsWith('data: ')) {39          try {40            const data = JSON.parse(line.slice(6));41            if (data.type === 'content_block_delta') {42              const content = data.delta?.text || '';43              fullContent += content;44              this.streamingContent.set(fullContent);45            }46          } catch {47            // Skip invalid JSON48          }49        }50      }51    }52 53    this.streamingContent.set('');54    return fullContent;55  }56}

Key Differences

  • Uses content_block_delta event type
  • Content in data.delta.text
  • Requires max_tokens parameter

Error Handling

Gracefully handle API errors

Robust error handling with typed errors and user-friendly messages:

1// chat.service.ts (with error handling)2import { Injectable, signal } from '@angular/core';3 4export interface ChatError {5  code: 'NETWORK' | 'RATE_LIMIT' | 'AUTH' | 'SERVER' | 'UNKNOWN';6  message: string;7  retryable: boolean;8}9 10@Injectable({ providedIn: 'root' })11export class ChatService {12  private error = signal<ChatError | null>(null);13  readonly error$ = this.error.asReadonly();14 15  async sendMessage(content: string): Promise<string> {16    this.error.set(null);17 18    try {19      const response = await fetch('/api/chat', {20        method: 'POST',21        headers: { 'Content-Type': 'application/json' },22        body: JSON.stringify({ messages: [{ role: 'user', content }] }),23      });24 25      if (!response.ok) {26        const error = await this.handleHttpError(response);27        this.error.set(error);28        throw new Error(error.message);29      }30 31      return await response.text();32    } catch (err) {33      if (err instanceof TypeError) {34        // Network error35        this.error.set({36          code: 'NETWORK',37          message: 'Unable to connect. Check your internet connection.',38          retryable: true,39        });40      }41      throw err;42    }43  }44 45  private async handleHttpError(response: Response): Promise<ChatError> {46    switch (response.status) {47      case 401:48        return {49          code: 'AUTH',50          message: 'Authentication failed. Please check your API key.',51          retryable: false,52        };53      case 429:54        return {55          code: 'RATE_LIMIT',56          message: 'Rate limit exceeded. Please wait a moment.',57          retryable: true,58        };59      case 500:60      case 502:61      case 503:62        return {63          code: 'SERVER',64          message: 'Server error. Please try again later.',65          retryable: true,66        };67      default:68        return {69          code: 'UNKNOWN',70          message: `Error: ${response.statusText}`,71          retryable: false,72        };73    }74  }75}76 77// Usage in component78@Component({79  template: `80    @if (error(); as err) {81      <div class="rounded-lg bg-destructive/10 p-4 text-destructive">82        <p>{{ err.message }}</p>83        @if (err.retryable) {84          <button (click)="retry()">Retry</button>85        }86      </div>87    }88  `,89})90export class ChatComponent {91  private chatService = inject(ChatService);92  error = this.chatService.error$;93 94  retry() {95    // Resend last message96  }97}

Rate Limiting

Client-side rate limiting to prevent API abuse

Implement client-side rate limiting to protect your API budget:

1// rate-limiter.service.ts2import { Injectable, signal } from '@angular/core';3 4@Injectable({ providedIn: 'root' })5export class RateLimiterService {6  private requestTimes: number[] = [];7  private readonly maxRequests = 10; // per minute8  private readonly windowMs = 60000;9 10  private isLimited = signal(false);11  readonly isLimited$ = this.isLimited.asReadonly();12 13  canMakeRequest(): boolean {14    const now = Date.now();15 16    // Remove old requests outside the window17    this.requestTimes = this.requestTimes.filter(18      time => now - time < this.windowMs19    );20 21    if (this.requestTimes.length >= this.maxRequests) {22      this.isLimited.set(true);23 24      // Calculate time until oldest request expires25      const oldestRequest = this.requestTimes[0];26      const resetTime = oldestRequest + this.windowMs - now;27 28      setTimeout(() => this.isLimited.set(false), resetTime);29      return false;30    }31 32    return true;33  }34 35  recordRequest() {36    this.requestTimes.push(Date.now());37  }38 39  getRemainingRequests(): number {40    const now = Date.now();41    const recentRequests = this.requestTimes.filter(42      time => now - time < this.windowMs43    );44    return Math.max(0, this.maxRequests - recentRequests.length);45  }46}47 48// Usage49async sendMessage(content: string) {50  if (!this.rateLimiter.canMakeRequest()) {51    this.error.set('Rate limited. Please wait.');52    return;53  }54 55  this.rateLimiter.recordRequest();56  // Make API call57}

Retry Logic

Automatic retries with exponential backoff

Retry failed requests automatically with exponential backoff:

1// retry.util.ts2export async function withRetry<T>(3  fn: () => Promise<T>,4  options: {5    maxRetries?: number;6    baseDelay?: number;7    maxDelay?: number;8    shouldRetry?: (error: Error) => boolean;9  } = {}10): Promise<T> {11  const {12    maxRetries = 3,13    baseDelay = 1000,14    maxDelay = 10000,15    shouldRetry = () => true,16  } = options;17 18  let lastError: Error;19 20  for (let attempt = 0; attempt <= maxRetries; attempt++) {21    try {22      return await fn();23    } catch (error) {24      lastError = error as Error;25 26      if (attempt === maxRetries || !shouldRetry(lastError)) {27        throw lastError;28      }29 30      // Exponential backoff with jitter31      const delay = Math.min(32        baseDelay * Math.pow(2, attempt) + Math.random() * 1000,33        maxDelay34      );35 36      await new Promise(resolve => setTimeout(resolve, delay));37    }38  }39 40  throw lastError!;41}42 43// Usage44const response = await withRetry(45  () => this.chatService.sendMessage(content),46  {47    maxRetries: 3,48    shouldRetry: (error) => {49      // Only retry on network or 5xx errors50      return error.message.includes('network') ||51             error.message.includes('5');52    },53  }54);

Token Counting

Estimate and manage token usage

Track token usage to stay within context limits and manage costs:

1// token-counter.service.ts2import { Injectable } from '@angular/core';3 4@Injectable({ providedIn: 'root' })5export class TokenCounterService {6  // Approximate token count (actual count requires tiktoken library)7  estimateTokens(text: string): number {8    // Rough estimate: ~4 characters per token for English9    return Math.ceil(text.length / 4);10  }11 12  // For accurate counting, use tiktoken13  // npm install @dqbd/tiktoken14  /*15  import { encoding_for_model } from '@dqbd/tiktoken';16 17  countTokensAccurate(text: string, model = 'gpt-4'): number {18    const enc = encoding_for_model(model);19    const tokens = enc.encode(text);20    enc.free();21    return tokens.length;22  }23  */24 25  // Check if within context limit26  isWithinLimit(27    messages: { content: string }[],28    maxTokens: number29  ): boolean {30    const totalTokens = messages.reduce(31      (sum, msg) => sum + this.estimateTokens(msg.content),32      033    );34    return totalTokens < maxTokens;35  }36 37  // Truncate messages to fit context38  truncateMessages(39    messages: { content: string }[],40    maxTokens: number41  ): { content: string }[] {42    const result: { content: string }[] = [];43    let currentTokens = 0;44 45    // Keep most recent messages46    for (let i = messages.length - 1; i >= 0; i--) {47      const tokens = this.estimateTokens(messages[i].content);48      if (currentTokens + tokens > maxTokens) break;49      result.unshift(messages[i]);50      currentTokens += tokens;51    }52 53    return result;54  }55}

Best Practices

Production-ready API integration tips

Security

  • Never expose API keys in frontend
  • Use backend proxy for all API calls
  • Implement authentication
  • Rate limit by user/IP

Performance

  • Use streaming for better UX
  • Cancel in-flight requests
  • Cache common responses
  • Implement request debouncing

Reliability

  • Implement retry with backoff
  • Handle partial responses
  • Fallback to non-streaming
  • Monitor error rates

Cost Control

  • Track token usage
  • Set usage limits
  • Truncate long conversations
  • Use cheaper models when appropriate

Next Steps

Continue building your chat application: