Claude API ile Akıllı Yazı Üretimi: React Uygulamalarında Yapay Zeka Destekli İçerik Oluşturma Rehberi
Yapay zeka destekli içerik üretimi, modern web uygulamalarının vazgeçilmez bir parçası haline geldi. Anthropic'in geliştirdiği Claude API, güçlü dil modeli yetenekleriyle geliştiricilere blog yazıları, ürün açıklamaları, e-posta taslakları ve çok daha fazlasını programatik olarak üretme imkânı sunuyor. Bu yazıda, Claude API'yi bir React uygulamasına nasıl entegre edeceğinizi, streaming yanıtlarla gerçek zamanlı içerik üretimini nasıl yapacağınızı ve production ortamında dikkat etmeniz gereken her şeyi detaylıca ele alacağız.
Claude API Nedir ve Neden Tercih Etmelisiniz?
Claude, Anthropic tarafından geliştirilen büyük bir dil modelidir (LLM). GPT serisine alternatif olarak öne çıkan Claude, özellikle şu avantajlarıyla dikkat çeker:
- Uzun bağlam penceresi: Claude 3.5 Sonnet ile 200K token'a kadar bağlam desteği
- Güvenlik odaklı tasarım: Constitutional AI yaklaşımıyla daha güvenli çıktılar
- Yüksek doğruluk: Karmaşık talimatları anlama ve takip etmede üstün performans
- Rekabetçi fiyatlandırma: Özellikle Claude 3 Haiku modeli ile düşük maliyetli çözümler
- Hızlı yanıt süreleri: Streaming desteği ile anlık içerik akışı
Geliştirme Ortamının Hazırlanması
1. API Anahtarı Edinme
İlk adım olarak Anthropic Console üzerinden bir hesap oluşturun ve API anahtarınızı alın. Anahtarınızı güvenli bir şekilde saklayın — asla frontend kodunda doğrudan kullanmayın.
2. Proje Kurulumu
Yeni bir React projesi oluşturup gerekli bağımlılıkları yükleyelim:
npx create-react-app claude-writer --template typescript
cd claude-writer
npm install @anthropic-ai/sdk express cors dotenv
npm install -D @types/express @types/cors nodemon concurrently3. Ortam Değişkenlerini Yapılandırma
Proje kök dizininde bir .env dosyası oluşturun:
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxxxxxx
PORT=3001⚠️ Güvenlik Uyarısı:
.envdosyanızı mutlaka.gitignoredosyasına ekleyin. API anahtarınız asla versiyon kontrol sistemine commit edilmemelidir.
Backend API Sunucusu Oluşturma
Claude API'ye doğrudan frontend'den istek göndermek güvenlik açığı oluşturur. Bu nedenle bir proxy backend oluşturmamız gerekiyor.
Express Sunucusu
server/index.js dosyasını oluşturun:
const express = require('express');
const cors = require('cors');
const Anthropic = require('@anthropic-ai/sdk');
require('dotenv').config();
const app = express();
app.use(cors({ origin: 'http://localhost:3000' }));
app.use(express.json());
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Standart yazı üretimi endpoint'i
app.post('/api/generate', async (req, res) => {
try {
const { prompt, contentType, tone, language, maxTokens } = req.body;
if (!prompt || prompt.trim().length === 0) {
return res.status(400).json({ error: 'Prompt alanı zorunludur.' });
}
const systemPrompt = buildSystemPrompt(contentType, tone, language);
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: maxTokens || 1024,
system: systemPrompt,
messages: [
{
role: 'user',
content: prompt,
},
],
});
res.json({
content: message.content[0].text,
usage: {
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
},
model: message.model,
});
} catch (error) {
console.error('Claude API Hatası:', error.message);
res.status(500).json({
error: 'İçerik üretimi sırasında bir hata oluştu.',
details: error.message,
});
}
});
function buildSystemPrompt(contentType, tone, language) {
const toneMap = {
professional: 'profesyonel ve resmi',
casual: 'samimi ve günlük',
academic: 'akademik ve bilimsel',
creative: 'yaratıcı ve etkileyici',
};
const contentTypeMap = {
blog: 'SEO dostu blog yazısı',
product: 'ürün açıklaması',
email: 'e-posta taslağı',
social: 'sosyal medya paylaşımı',
summary: 'özet',
};
return `Sen deneyimli bir ${language || 'Türkçe'} içerik yazarısın.
${contentTypeMap[contentType] || 'genel içerik'} formatında,
${toneMap[tone] || 'profesyonel'} bir üslupla yazı üretiyorsun.
Markdown formatında, okunabilir ve akıcı içerikler oluştur.
Her zaman doğru bilgi ver ve gerektiğinde alt başlıklar kullan.`;
}
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`✅ Sunucu http://localhost:${PORT} adresinde çalışıyor`);
});Streaming ile Gerçek Zamanlı İçerik Akışı
Kullanıcı deneyimini dramatik şekilde iyileştiren streaming özelliği, içeriğin parça parça aktarılmasını sağlar. Kullanıcı, tüm yanıtın oluşturulmasını beklemek yerine, metnin gerçek zamanlı olarak yazıldığını görür.
Streaming Endpoint
// server/index.js dosyasına ekleyin
app.post('/api/generate-stream', async (req, res) => {
try {
const { prompt, contentType, tone, language, maxTokens } = req.body;
// SSE (Server-Sent Events) başlıkları
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const systemPrompt = buildSystemPrompt(contentType, tone, language);
const stream = await anthropic.messages.stream({
model: 'claude-sonnet-4-20250514',
max_tokens: maxTokens || 2048,
system: systemPrompt,
messages: [
{
role: 'user',
content: prompt,
},
],
});
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
res.write(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`);
}
}
// Akışın bittiğini bildir
const finalMessage = await stream.finalMessage();
res.write(
`data: ${JSON.stringify({
done: true,
usage: {
inputTokens: finalMessage.usage.input_tokens,
outputTokens: finalMessage.usage.output_tokens,
},
})}\n\n`
);
res.end();
} catch (error) {
console.error('Streaming hatası:', error.message);
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});React Frontend Uygulaması
Şimdi kullanıcıların yazı parametrelerini belirleyip içerik üretebileceği bir React arayüzü oluşturalım.
Custom Hook: useClaudeWriter
// src/hooks/useClaudeWriter.ts
import { useState, useCallback, useRef } from 'react';
interface GenerateOptions {
prompt: string;
contentType: 'blog' | 'product' | 'email' | 'social' | 'summary';
tone: 'professional' | 'casual' | 'academic' | 'creative';
language: string;
maxTokens: number;
}
interface UsageInfo {
inputTokens: number;
outputTokens: number;
}
export function useClaudeWriter() {
const [content, setContent] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [usage, setUsage] = useState<UsageInfo | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const generateStream = useCallback(async (options: GenerateOptions) => {
setIsLoading(true);
setError(null);
setContent('');
setUsage(null);
// Önceki isteği iptal et
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
try {
const response = await fetch('http://localhost:3001/api/generate-stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(options),
signal: abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP Hatası: ${response.status}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error('Stream okunamıyor');
}
let accumulatedContent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.error) {
setError(data.error);
break;
}
if (data.done) {
setUsage(data.usage);
break;
}
if (data.text) {
accumulatedContent += data.text;
setContent(accumulatedContent);
}
} catch {
// JSON parse hatalarını yoksay (eksik parçalar)
}
}
}
}
} catch (err: any) {
if (err.name !== 'AbortError') {
setError(err.message || 'Bilinmeyen bir hata oluştu');
}
} finally {
setIsLoading(false);
}
}, []);
const cancel = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
setIsLoading(false);
}
}, []);
const reset = useCallback(() => {
setContent('');
setError(null);
setUsage(null);
}, []);
return {
content,
isLoading,
error,
usage,
generateStream,
cancel,
reset,
};
}Ana Bileşen: ClaudeWriter
// src/components/ClaudeWriter.tsx
import React, { useState } from 'react';
import { useClaudeWriter } from '../hooks/useClaudeWriter';
import ReactMarkdown from 'react-markdown';
export function ClaudeWriter() {
const [prompt, setPrompt] = useState('');
const [contentType, setContentType] = useState<'blog' | 'product' | 'email' | 'social' | 'summary'>('blog');
const [tone, setTone] = useState<'professional' | 'casual' | 'academic' | 'creative'>('professional');
const [maxTokens, setMaxTokens] = useState(1024);
const {
content,
isLoading,
error,
usage,
generateStream,
cancel,
reset,
} = useClaudeWriter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!prompt.trim()) return;
await generateStream({
prompt,
contentType,
tone,
language: 'Türkçe',
maxTokens,
});
};
const estimatedCost = usage
? (
(usage.inputTokens * 0.003 + usage.outputTokens * 0.015) /
1000
).toFixed(4)
: null;
return (
<div className="claude-writer">
<h1>🤖 Claude Yazı Üretici</h1>
<form onSubmit={handleSubmit} className="writer-form">
<div className="form-group">
<label htmlFor="prompt">Ne hakkında yazayım?</label>
<textarea
id="prompt"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Örn: React 19'un yeni özellikleri hakkında detaylı bir blog yazısı yaz..."
rows={4}
disabled={isLoading}
/>
</div>
<div className="form-