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_deltaevent type - Content in
data.delta.text - Requires
max_tokensparameter
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: