Node.js Uygulamasına OpenAI API Entegrasyonu
Yapay zeka entegrasyonu artık lüks değil, rekabetçi kalmak için neredeyse zorunlu hale geldi. Node.js ekosistemi ise bu entegrasyon için mükemmel bir zemin sunuyor: asenkron yapısı, zengin paket ekosistemi ve JavaScript’in yaygınlığı sayesinde OpenAI API’yi uygulamana dahil etmek düşündüğünden çok daha hızlı oluyor. Bu yazıda sıfırdan başlayarak production-ready bir entegrasyon nasıl kurarsın, hangi hataları yapmamalısın ve gerçek dünya senaryolarında ne tür optimizasyonlar gerekir, bunların hepsini ele alacağız.
Ortamı Hazırlamak
Önce temel kurulumu halledelim. Node.js 18+ kullanıyorsan fetch API native olarak geliyor, ama OpenAI’nin resmi kütüphanesini kullanmak çok daha mantıklı çünkü rate limiting, retry logic ve tip desteği gibi şeylerle tek tek uğraşmak zorunda kalmıyorsun.
mkdir openai-entegrasyon && cd openai-entegrasyon
npm init -y
npm install openai dotenv express
npm install -D typescript @types/node @types/express ts-node nodemon
Proje yapısını şimdiden düzenli tutmak ilerleyen süreçte çok işine yarayacak:
mkdir -p src/{routes,services,middleware,utils}
touch src/index.ts src/services/openai.service.ts .env .gitignore
.env dosyana API anahtarını ekle, ama bu dosyayı asla Git’e commit etme:
# .env
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx
OPENAI_MODEL=gpt-4o-mini
OPENAI_MAX_TOKENS=1000
PORT=3000
.gitignore dosyasına şunları ekle:
echo "node_modules/n.envndist/n*.log" > .gitignore
OpenAI Servis Katmanı Oluşturmak
Doğrudan controller’lardan API çağrısı yapmak kötü bir pratik. Servis katmanı ayırımı hem test edilebilirlik sağlar hem de ileride provider değiştirmek istediğinde hayatını kurtarır.
// src/services/openai.service.ts
import OpenAI from 'openai';
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
class OpenAIService {
private client: OpenAI;
private model: string;
private maxTokens: number;
constructor() {
if (!process.env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY environment variable tanımlanmamış');
}
this.client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
timeout: 30000,
maxRetries: 3,
});
this.model = process.env.OPENAI_MODEL || 'gpt-4o-mini';
this.maxTokens = parseInt(process.env.OPENAI_MAX_TOKENS || '1000');
}
async chat(
messages: ChatCompletionMessageParam[],
options?: {
temperature?: number;
maxTokens?: number;
systemPrompt?: string;
}
): Promise<string> {
const fullMessages: ChatCompletionMessageParam[] = [];
if (options?.systemPrompt) {
fullMessages.push({
role: 'system',
content: options.systemPrompt,
});
}
fullMessages.push(...messages);
try {
const response = await this.client.chat.completions.create({
model: this.model,
messages: fullMessages,
max_tokens: options?.maxTokens || this.maxTokens,
temperature: options?.temperature ?? 0.7,
});
const content = response.choices[0]?.message?.content;
if (!content) {
throw new Error('API boş yanıt döndürdü');
}
return content;
} catch (error) {
if (error instanceof OpenAI.APIError) {
console.error(`OpenAI API Hatası: ${error.status} - ${error.message}`);
throw error;
}
throw error;
}
}
async *chatStream(
messages: ChatCompletionMessageParam[],
systemPrompt?: string
): AsyncGenerator<string> {
const fullMessages: ChatCompletionMessageParam[] = [];
if (systemPrompt) {
fullMessages.push({ role: 'system', content: systemPrompt });
}
fullMessages.push(...messages);
const stream = await this.client.chat.completions.create({
model: this.model,
messages: fullMessages,
max_tokens: this.maxTokens,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
}
export const openAIService = new OpenAIService();
Bu yapıda maxRetries: 3 özellikle önemli. OpenAI’nin API’si zaman zaman geçici hatalar döndürebiliyor, bu ayar otomatik olarak yeniden deniyor ve sen bununla ilgilenmek zorunda kalmıyorsun.
Express API Endpoint’lerini Kurmak
Servis katmanını hazırladıktan sonra HTTP endpoint’lerini ekleyelim:
// src/routes/chat.routes.ts
import { Router, Request, Response } from 'express';
import { openAIService } from '../services/openai.service';
const router = Router();
// Basit chat endpoint
router.post('/chat', async (req: Request, res: Response) => {
try {
const { message, conversationHistory, systemPrompt } = req.body;
if (!message || typeof message !== 'string') {
return res.status(400).json({ error: 'message alanı zorunlu' });
}
const messages = [
...(conversationHistory || []),
{ role: 'user' as const, content: message },
];
const response = await openAIService.chat(messages, { systemPrompt });
return res.json({
success: true,
response,
model: process.env.OPENAI_MODEL,
});
} catch (error: any) {
console.error('Chat endpoint hatası:', error);
if (error.status === 429) {
return res.status(429).json({ error: 'Rate limit aşıldı, lütfen bekleyin' });
}
if (error.status === 401) {
return res.status(500).json({ error: 'API yapılandırma hatası' });
}
return res.status(500).json({ error: 'İç sunucu hatası' });
}
});
// Streaming endpoint
router.post('/chat/stream', async (req: Request, res: Response) => {
const { message, systemPrompt } = req.body;
if (!message) {
return res.status(400).json({ error: 'message zorunlu' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const generator = openAIService.chatStream(
[{ role: 'user', content: message }],
systemPrompt
);
for await (const chunk of generator) {
res.write(`data: ${JSON.stringify({ content: chunk })}nn`);
}
res.write('data: [DONE]nn');
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ error: 'Stream hatası oluştu' })}nn`);
res.end();
}
});
export default router;
Gerçek Dünya Senaryosu: Müşteri Destek Botu
Teorik örnekler güzel ama gerçek bir kullanım senaryosu olmadan pek bir işe yaramıyor. Diyelim ki bir e-ticaret sitesi için müşteri destek botu yapıyorsun. Bu bot şirketin ürünleri hakkında bilgi verecek ve sipariş takibi yapacak.
// src/services/support.service.ts
import { openAIService } from './openai.service';
interface OrderInfo {
orderId: string;
status: string;
estimatedDelivery: string;
items: string[];
}
// Gerçek uygulamada bu veriler veritabanından gelir
const mockOrders: Record<string, OrderInfo> = {
'ORD-1234': {
orderId: 'ORD-1234',
status: 'Kargoda',
estimatedDelivery: '2024-12-20',
items: ['Laptop Çantası', 'Mouse Pad'],
},
};
export class SupportService {
private systemPrompt = `Sen TechStore'un müşteri destek asistanısın.
Türkçe yanıt ver, nazik ve yardımsever ol.
Sipariş bilgilerini sana context olarak vereceğim.
Bilmediğin konularda "Bu konuda size yardımcı olamıyorum, lütfen
destek hattımızı arayın: 0850-XXX-XXXX" de.
Asla uydurmaca bilgi verme.`;
async handleSupportQuery(
userMessage: string,
orderId?: string,
conversationHistory?: Array<{ role: string; content: string }>
): Promise<string> {
let contextualPrompt = this.systemPrompt;
if (orderId && mockOrders[orderId]) {
const order = mockOrders[orderId];
contextualPrompt += `nnMüşterinin sipariş bilgileri:
- Sipariş No: ${order.orderId}
- Durum: ${order.status}
- Tahmini Teslimat: ${order.estimatedDelivery}
- Ürünler: ${order.items.join(', ')}`;
}
const messages = [
...(conversationHistory || []).map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
{ role: 'user' as const, content: userMessage },
];
return await openAIService.chat(messages, {
systemPrompt: contextualPrompt,
temperature: 0.3, // Destek botları için düşük temperature daha tutarlı yanıtlar verir
});
}
}
export const supportService = new SupportService();
Rate Limiting ve Maliyet Kontrolü
Production ortamında en çok başa çıkman gereken iki şey: rate limiting ve maliyet kontrolü. OpenAI’ye her istek para harcıyor ve kontrolsüz bırakırsan fatura sürpriz yapabilir.
// src/middleware/rateLimit.middleware.ts
import { Request, Response, NextFunction } from 'express';
interface RateLimitEntry {
count: number;
resetTime: number;
tokenCount: number;
}
const rateLimitStore = new Map<string, RateLimitEntry>();
// Dakikada maksimum 20 istek, 50.000 token
const MAX_REQUESTS_PER_MINUTE = 20;
const MAX_TOKENS_PER_MINUTE = 50000;
const WINDOW_MS = 60 * 1000;
export function aiRateLimit(req: Request, res: Response, next: NextFunction) {
// Gerçek uygulamada IP yerine kullanıcı ID'si kullan
const identifier = req.ip || 'unknown';
const now = Date.now();
let entry = rateLimitStore.get(identifier);
if (!entry || now > entry.resetTime) {
entry = {
count: 0,
resetTime: now + WINDOW_MS,
tokenCount: 0,
};
}
if (entry.count >= MAX_REQUESTS_PER_MINUTE) {
const retryAfter = Math.ceil((entry.resetTime - now) / 1000);
res.setHeader('Retry-After', retryAfter);
return res.status(429).json({
error: 'Çok fazla istek gönderdiniz',
retryAfter,
});
}
entry.count++;
rateLimitStore.set(identifier, entry);
// Periyodik temizlik (memory leak önlemi)
if (rateLimitStore.size > 10000) {
const oldEntries = [...rateLimitStore.entries()].filter(
([, v]) => now > v.resetTime
);
oldEntries.forEach(([k]) => rateLimitStore.delete(k));
}
next();
}
Redis kullanıyorsan in-memory store yerine Redis’e geçmek çok daha sağlıklı çünkü uygulamanı birden fazla instance’ta çalıştırdığında in-memory store tutarsız davranır.
Input Validasyonu ve Prompt Injection Koruması
Kullanıcıdan gelen inputu direkt olarak AI’ya göndermek ciddi bir güvenlik açığı. Prompt injection saldırıları gerçek bir tehdit ve buna karşı önlem almak şart.
// src/utils/inputSanitizer.ts
const INJECTION_PATTERNS = [
/ignore (all |previous |above )?instructions/i,
/you are now/i,
/pretend (you are|to be)/i,
/disregard (your|all) (previous |training |guidelines)/i,
/system prompt/i,
/[INST]/i,
/<<SYS>>/i,
];
export function sanitizeUserInput(input: string): {
safe: boolean;
sanitized: string;
reason?: string;
} {
// Uzunluk kontrolü
if (input.length > 4000) {
return {
safe: false,
sanitized: '',
reason: 'Mesaj çok uzun (max 4000 karakter)',
};
}
// Injection pattern kontrolü
for (const pattern of INJECTION_PATTERNS) {
if (pattern.test(input)) {
return {
safe: false,
sanitized: '',
reason: 'Geçersiz mesaj içeriği',
};
}
}
// Temel temizlik
const sanitized = input
.trim()
.replace(//g, '') // null bytes
.slice(0, 4000);
return { safe: true, sanitized };
}
export function estimateTokenCount(text: string): number {
// Kaba tahmin: ortalama 4 karakter = 1 token
return Math.ceil(text.length / 4);
}
Konuşma Geçmişi Yönetimi
ChatGPT gibi bir deneyim için konuşma geçmişini doğru yönetmek kritik. Ama her mesajda tüm geçmişi göndermek hem maliyetli hem de token limitine takılma riski taşıyor.
// src/utils/conversationManager.ts
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
interface ConversationSession {
messages: ChatCompletionMessageParam[];
totalTokens: number;
createdAt: Date;
lastActivity: Date;
}
const sessions = new Map<string, ConversationSession>();
const MAX_CONTEXT_TOKENS = 8000;
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 dakika
export class ConversationManager {
getOrCreateSession(sessionId: string): ConversationSession {
let session = sessions.get(sessionId);
if (!session) {
session = {
messages: [],
totalTokens: 0,
createdAt: new Date(),
lastActivity: new Date(),
};
sessions.set(sessionId, session);
}
session.lastActivity = new Date();
return session;
}
addMessage(
sessionId: string,
role: 'user' | 'assistant',
content: string
): ChatCompletionMessageParam[] {
const session = this.getOrCreateSession(sessionId);
const tokenEstimate = Math.ceil(content.length / 4);
session.messages.push({ role, content });
session.totalTokens += tokenEstimate;
// Token limiti aşılıyorsa eski mesajları sil (ilk mesajı koru)
while (session.totalTokens > MAX_CONTEXT_TOKENS && session.messages.length > 2) {
const removed = session.messages.splice(0, 2); // En eski user-assistant çiftini sil
removed.forEach((m) => {
session.totalTokens -= Math.ceil((m.content as string).length / 4);
});
}
return session.messages;
}
cleanupExpiredSessions(): void {
const now = Date.now();
for (const [id, session] of sessions.entries()) {
if (now - session.lastActivity.getTime() > SESSION_TIMEOUT_MS) {
sessions.delete(id);
}
}
}
}
export const conversationManager = new ConversationManager();
// Her 15 dakikada expired session'ları temizle
setInterval(() => {
conversationManager.cleanupExpiredSessions();
}, 15 * 60 * 1000);
Ana Uygulama ve Tüm Parçaları Birleştirmek
// src/index.ts
import express from 'express';
import dotenv from 'dotenv';
import chatRoutes from './routes/chat.routes';
import { aiRateLimit } from './middleware/rateLimit.middleware';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json({ limit: '10kb' })); // Büyük payload saldırılarına karşı
// Global middleware
app.use('/api/ai', aiRateLimit);
// Routes
app.use('/api/ai', chatRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
apiConfigured: !!process.env.OPENAI_API_KEY,
});
});
// Error handler
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Beklenmeyen hata:', err);
res.status(500).json({ error: 'Sunucu hatası' });
});
app.listen(PORT, () => {
console.log(`Server ${PORT} portunda çalışıyor`);
console.log(`OpenAI Model: ${process.env.OPENAI_MODEL}`);
});
package.json‘a script ekle:
# package.json scripts kısmına ekle:
# "dev": "nodemon --exec ts-node src/index.ts",
# "build": "tsc",
# "start": "node dist/index.js"
npm run dev
API’yi test etmek için:
# Basit chat isteği
curl -X POST http://localhost:3000/api/ai/chat
-H "Content-Type: application/json"
-d '{"message": "Node.js nedir? Kısaca anlat."}'
# Streaming test
curl -X POST http://localhost:3000/api/ai/chat/stream
-H "Content-Type: application/json"
-d '{"message": "Linux sistem yönetimi hakkında 5 ipucu ver"}'
--no-buffer
Production’da Dikkat Edilmesi Gerekenler
API Key Güvenliği: API anahtarını sadece backend’de tut, frontend’e asla expose etme. Eğer frontend’den direkt çağrı yapman gerekiyorsa OpenAI’nin usage limits özelliğini kullan ve ayrı bir proxy backend oluştur.
Hata Loglaması: OpenAI API hataları için ayrı log kanalı oluştur. Hangi promptlar hata veriyor, hangi kullanıcılar limit aşıyor bunları görmek hem debug hem de maliyet optimizasyonu için kritik.
Caching Stratejisi: Aynı sorular tekrar tekrar soruluyorsa (FAQ tipi) Redis’te cache tutmak hem maliyeti düşürür hem de latency’yi azaltır. Semantic caching için embedding benzerliği kullanabilirsin ama bu başlı başına ayrı bir konu.
Token Monitoring: Her response’dan usage bilgisini logla. OpenAI SDK bunu response.usage olarak döndürüyor. Aylık harcama tahminleri yapmak için bu veriler altın değerinde.
Timeout Yönetimi: 30 saniyelik timeout çoğu durum için yeterli ama streaming için bu değeri artırman gerekebilir. Nginx veya load balancer kullanıyorsan onların da timeout ayarlarını güncellemeyi unutma.
Model Seçimi: gpt-4o-mini fiyat/performans açısından çoğu kullanım senaryosu için ideal başlangıç noktası. Gerçekten karmaşık reasoning gerektiren durumlar için gpt-4o‘ya geç. Her şey için en pahalı modeli kullanmak hem israf hem de yavaşlık.
Sonuç
Node.js ile OpenAI entegrasyonu teknik olarak basit görünse de production’da karşılaşılan sorunların büyük çoğunluğu tasarım kararlarından kaynaklanıyor. Servis katmanı ayrımı, input validasyonu, konuşma geçmişi yönetimi ve maliyet kontrolü en baştan düşünülmezse sonradan refactor etmek ciddi zaman alıyor.
Bu yazıda anlattığım yaklaşım küçük bir MVP’den başlayıp production seviyesine çıkabilecek şekilde tasarlandı. Rate limiting için Redis’e geçmek, monitoring için Prometheus metrikleri eklemek, caching katmanı eklemek gibi konular bir sonraki adım olarak ele alınabilir. Ama temeller sağlam kurulursa bu adımları atmak çok daha az sancılı oluyor.
OpenAI API’sinin dokümantasyonu oldukça iyi yazılmış, resmi TypeScript SDK’sı da tip güvenliği açısından mükemmel destek veriyor. Takıldığın noktalarda doğrudan SDK kaynak koduna bakmayı ihmal etme, çoğu sorunun cevabı orada gizli.
