TypeScript için CloudEvents SDK: Serverless Mimarilerde Event Standardizasyonu
CloudEvents spesifikasyonu ve TypeScript SDK'sı için pratik rehber: AWS Lambda ve EventBridge'de standart eventler oluştur, parse et ve doğrula.
Event-driven mimarilerde her event kaynağı event’leri farklı şekilde tanımlama eğilimindedir: bir Lambda { userId: string } beklerken, diğeri { user_id: string }, üçüncüsü ise { sub: string } kullanır. Bu heterojenliğin maliyeti, kaynak sayısıyla büyüyen entegrasyon kodu ve sistemler arasında event’leri ilişkilendiremeyen gözlemleme araçlarıdır. Standartlaştırılmış bir event zarfı her iki sorunu da çözer; karşılığında her üreticinin aynı şemayı benimsemesini gerektirir.
CloudEvents, bu zarf için CNCF spesifikasyonudur; TypeScript SDK’sı da onu AWS Lambda üzerinde pratik varsayılan haline getirir: tipli bir CloudEvent sınıfı, iki uçta da HTTP binding ile parse ve EventBridge, SNS, SQS için düz JSON serileştirme. Geçiş kademeli olabilir; zarfı önce yeni event tipleri alır, mevcut producerlar sonra gelir.
Event Standardizasyonunun Önemi#
Event-driven sistemler loose coupling ile gelişir, ancak her producer kendi event formatını icat ettiğinde sorunlar yaşar. Zorluklar hızla birikiyor:
- Her event source için özel parsing logic
- Farklı servisler arası ortak tooling yok
- Eventler beklentilere uymadığında debug zorluğu
- Event producerları değiştiğinde migration sürtünmesi
CloudEvents dördüne birden tek bir zarf şekliyle yanıt veriyor. Transport EventBridge, Kafka veya Kinesis olsun, aynı specversion, type ve source attributeları tek bir parser ve tek bir araç setinin her kaynağı işlemesini sağlıyor.
CloudEvents Spesifikasyonu#
CloudEvents, eventleri tanımlayan standart metadata attributelarını belirliyor:
{
"specversion": "1.0", // CloudEvents versiyonu
"type": "com.example.order.created", // Event tipi
"source": "/orders/service", // Event producer
"id": "A234-1234-1234", // Unique event ID
"time": "2025-10-29T12:00:00Z", // Ne zaman oldu
"datacontenttype": "application/json", // Payload formatı
"data": { // Asıl payload
"orderId": "12345",
"amount": 99.99
}
}
Bu yapı event metadatasını (kim, ne, ne zaman) event datadan (payload) ayırarak, tüm payloadı parse etmeden eventleri route etmeyi, filtrelemeyi ve işlemeyi kolaylaştırıyor.
CloudEvents SDK Kurulumu#
JavaScript SDK, TypeScript tanımlamaları sağlıyor ve Node.js 18+ ile çalışıyor (Node.js 22 öneriliyor):
npm install cloudevents
SDK hafif (harici HTTP dependency’si yok) ve CloudEvents v1.0 spesifikasyonunu destekliyor.
TypeScript’te CloudEvent Oluşturma#
SDK, tam TypeScript desteği olan bir CloudEvent class’ı sağlıyor:
import { CloudEvent } from 'cloudevents';
// Basit event oluşturma
const event = new CloudEvent({
type: 'com.example.user.created',
source: '/users/service',
data: {
userId: '12345',
email: 'user@example.com',
createdAt: new Date().toISOString()
}
});
console.log(event.id); // Otomatik generate edilen UUID
console.log(event.time); // Otomatik generate edilen timestamp
Event Data için Type Safety#
Event datası için generic typelar sağlayabilirsin:
interface OrderCreatedData {
orderId: string;
customerId: string;
amount: number;
currency: string;
}
const orderEvent = new CloudEvent<OrderCreatedData>({
type: 'com.example.order.created',
source: '/orders/service',
data: {
orderId: '12345',
customerId: 'cust-789',
amount: 99.99,
currency: 'USD'
}
});
// TypeScript orderEvent.data'nın şeklini biliyor
const amount: number = orderEvent.data!.amount;
Dikkat: CloudEvent objeleri immutable. Bir eventi değiştirmek için cloneWith() methodunu kullan:
const updatedEvent = orderEvent.cloneWith({
data: {
...orderEvent.data!,
amount: 149.99 // Güncellenmiş miktar
}
});
Gelen CloudEventleri Parse Etme#
AWS Lambda’da eventleri alırken, gelen requestleri parse etmek için HTTP binding kullan:
import { HTTP } from 'cloudevents';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
// HTTP requestinden CloudEvent parse et
const cloudEvent = HTTP.toEvent({
headers: event.headers,
body: event.body
});
console.log('CloudEvent alındı:', {
type: cloudEvent.type,
source: cloudEvent.source,
id: cloudEvent.id
});
// Event datasına eriş
const payload = cloudEvent.data;
return {
statusCode: 200,
body: JSON.stringify({ message: 'Event işlendi' })
};
} catch (error) {
console.error('CloudEvent parse edilemedi:', error);
return {
statusCode: 400,
body: JSON.stringify({ error: 'Geçersiz CloudEvent' })
};
}
};
HTTP.toEvent() methodu hem binary hem structured content modlarını destekliyor ve headerlara göre formatı otomatik algılıyor.
CloudEventleri Validate Etme#
SDK, CloudEventleri spesifikasyona göre validate ediyor. Gerekli attributelar:
type: Event tipi tanımlayıcısısource: Event producer tanımlayıcısıspecversion: CloudEvents versiyonu (varsayılan “1.0”)
import { CloudEvent, ValidationError } from 'cloudevents';
function validateAndProcess(eventData: unknown) {
try {
// Gerekli attributelar eksikse throw eder
const event = new CloudEvent(eventData as any);
// Ek custom validation
if (!event.type.startsWith('com.example.')) {
throw new Error('Event type com.example. ile başlamalı.');
}
return { valid: true, event };
} catch (error) {
if (error instanceof ValidationError) {
console.error('CloudEvent validation başarısız:', error.message);
return { valid: false, error: error.message };
}
throw error;
}
}
AWS Lambda Entegrasyon Patternleri#
Pattern 1: API Gateway Endpoint ile CloudEvents#
import { HTTP } from 'cloudevents';
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
interface OrderData {
orderId: string;
status: 'pending' | 'completed' | 'failed';
}
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
// Gelen CloudEvent'i parse et
const cloudEvent = HTTP.toEvent<OrderData>({
headers: event.headers,
body: event.body
});
// Event datasına type-safe erişim
const orderData = cloudEvent.data!;
console.log(`${orderData.orderId} siparişi ${orderData.status} statusu ile işleniyor`);
// Business logic buraya
await processOrder(orderData);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: 'Sipariş işlendi',
eventId: cloudEvent.id
})
};
};
async function processOrder(data: OrderData): Promise<void> {
// Implementation detayları
}
Pattern 2: EventBridge’e CloudEvent Publish Etme#
AWS EventBridge natively CloudEvents formatını desteklemiyor, ancak CloudEventleri detail fieldına embed edebilirsin:
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
import { CloudEvent } from 'cloudevents';
const eventBridge = new EventBridgeClient({});
async function publishToEventBridge(cloudEvent: CloudEvent) {
// EventBridge belirli bir format bekliyor
const command = new PutEventsCommand({
Entries: [
{
Source: cloudEvent.source,
DetailType: cloudEvent.type,
Detail: JSON.stringify({
// CloudEvent'i detail fieldına embed et
cloudevents: {
specversion: cloudEvent.specversion,
id: cloudEvent.id,
time: cloudEvent.time,
type: cloudEvent.type,
source: cloudEvent.source,
datacontenttype: cloudEvent.datacontenttype,
data: cloudEvent.data
}
}),
EventBusName: 'default'
}
]
});
const response = await eventBridge.send(command);
if (response.FailedEntryCount && response.FailedEntryCount > 0) {
throw new Error(`Event publish edilemedi: ${JSON.stringify(response.Entries)}`);
}
return response;
}
Pattern 3: SNS/SQS ile CloudEvents#
SNS ve SQS için CloudEventleri JSON’a serialize et:
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
import { CloudEvent } from 'cloudevents';
const sns = new SNSClient({});
async function publishToSNS(cloudEvent: CloudEvent, topicArn: string) {
// CloudEvent'i JSON'a serialize et
const message = JSON.stringify({
specversion: cloudEvent.specversion,
type: cloudEvent.type,
source: cloudEvent.source,
id: cloudEvent.id,
time: cloudEvent.time,
data: cloudEvent.data
});
const command = new PublishCommand({
TopicArn: topicArn,
Message: message,
MessageAttributes: {
'content-type': {
DataType: 'String',
StringValue: 'application/cloudevents+json'
},
'ce-type': {
DataType: 'String',
StringValue: cloudEvent.type
},
'ce-source': {
DataType: 'String',
StringValue: cloudEvent.source
}
}
});
return sns.send(command);
}
Dikkat: SQS’ten consume ederken SNS message wrapper’ını parse etmeyi unutma:
import { SQSEvent } from 'aws-lambda';
import { CloudEvent } from 'cloudevents';
export const handler = async (event: SQSEvent) => {
for (const record of event.Records) {
// SQS recordundan SNS message parse et
const snsMessage = JSON.parse(record.body);
const cloudEventData = JSON.parse(snsMessage.Message);
// CloudEvent'i yeniden oluştur
const cloudEvent = new CloudEvent(cloudEventData);
console.log('Event işleniyor:', cloudEvent.type);
// Eventi işle...
}
};
Type Safety Faydaları#
TypeScript’in type sistemi CloudEvents ile iyi çalışıyor:
import { CloudEvent, CloudEventV1 } from 'cloudevents';
// Event tiplerini tanımla
type UserCreated = CloudEventV1<{
userId: string;
email: string;
plan: 'free' | 'pro' | 'enterprise';
}>;
type OrderPlaced = CloudEventV1<{
orderId: string;
customerId: string;
items: Array<{ sku: string; quantity: number }>;
}>;
// Type-safe event handler
function handleUserCreated(event: UserCreated) {
const { userId, email, plan } = event.data!;
// TypeScript tam şekli biliyor
if (plan === 'enterprise') {
// Onboarding flow'u tetikle
}
}
function handleOrderPlaced(event: OrderPlaced) {
const { orderId, items } = event.data!;
// items tipi: Array<{ sku: string; quantity: number }>
console.log(`${orderId} siparişinde ${items.length} kalem var`);
}
// Type narrowing ile generic event router
function routeEvent(event: CloudEvent) {
switch (event.type) {
case 'com.example.user.created':
handleUserCreated(event as UserCreated);
break;
case 'com.example.order.placed':
handleOrderPlaced(event as OrderPlaced);
break;
default:
console.warn('Bilinmeyen event tipi:', event.type);
}
}
Production Mimari Örneği#
CloudEvents kullanan pratik bir event-driven mimari:
Best Practice’ler ve Uygulama Notları#
1. Tutarlı Type İsimlendirme Kullan#
Event tipleri için reverse-DNS isimlendirme konvansiyonu benimse:
// İyi: Açık hiyerarşi ve ownership
'com.company.service.entity.action'
'com.example.orders.order.created'
'com.example.users.user.updated'
// Kötü: Generic veya belirsiz isimler
'order-created'
'user_event'
'notification'
2. Event Schemalarını Versiyonla#
Event tipinde versiyon bilgisi içer:
const event = new CloudEvent({
type: 'com.example.orders.v1.order.created', // tipte v1
source: '/orders/service',
data: {
// v1 schema: orderId + amount
orderId: '12345',
amount: 99.99
}
});
// Schema değiştiğinde
const eventV2 = new CloudEvent({
type: 'com.example.orders.v2.order.created', // v2 tipi
source: '/orders/service',
data: {
// breaking change'li v2 schema
orderId: '12345',
totalAmount: 99.99,
currency: 'USD' // yeni required field
}
});
3. Debug için Eventleri Sakla#
CloudEvents, event sourcing ve audit trailleri kolaylaştırıyor:
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { CloudEvent } from 'cloudevents';
const dynamodb = new DynamoDBClient({});
async function storeEvent(event: CloudEvent) {
const command = new PutItemCommand({
TableName: 'EventStore',
Item: {
eventId: { S: event.id },
eventType: { S: event.type },
eventSource: { S: event.source },
timestamp: { S: event.time || new Date().toISOString() },
data: { S: JSON.stringify(event.data) },
// Otomatik cleanup için TTL ekle
ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) }
}
});
await dynamodb.send(command);
}
4. Büyük Payloadları Handle Et#
CloudEvents datası büyüyebilir. Dosyalar veya büyük datasetler için claim check patternini kullan:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({});
async function createEventWithLargePayload(largeData: unknown) {
// Büyük payloadı S3'e sakla
const dataKey = `events/${Date.now()}-${Math.random()}.json`;
await s3.send(new PutObjectCommand({
Bucket: 'event-payloads',
Key: dataKey,
Body: JSON.stringify(largeData),
ContentType: 'application/json'
}));
// Reference ile CloudEvent oluştur
return new CloudEvent({
type: 'com.example.data.processed',
source: '/data/processor',
data: {
payloadLocation: `s3://event-payloads/${dataKey}`,
payloadSize: JSON.stringify(largeData).length
}
});
}
5. Extension Attributeları Dikkatli Kullan#
CloudEvents custom extension attributeları destekliyor, ama dikkatli kullan:
// Tracing için extension attributelar
const event = new CloudEvent({
type: 'com.example.order.created',
source: '/orders/service',
data: { orderId: '12345' },
// Dokümante edilmiş tracing extensionı, business logic yeri değil
traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
tracestate: 'congo=t61rcWkgMzE'
});
Not: traceparent ve tracestate, spesifikasyonun Distributed Tracing extensionından gelir; extensionı bilen araçlar bunları okuyabilir. İkisi de zorunlu değildir: trace context’i olmayan bir producer bu attributeları hiç göndermez, bu yüzden consumerlar eventi reddetmek yerine eksik değeri handle etmeli. Kendi uydurduğun extension isimlerinin böyle bir karşılığı yoktur, bu yüzden domain’e özgü datayı data fieldına koymayı tercih et.
Standardizasyon Neyi Değiştirir#
Ortak bir zarf yokken yeni bir event consumer eklemek şunları gerektirir:
- Dokümantasyon bulmak (genelde güncel değil)
- Custom event formatını anlamak
- O formata özel parsing logic yazmak
- O formatın izin verdiği edge caseleri test etmek
CloudEvents ile aynı iş şuna dönüşür:
- Event type ve source’u kontrol et
- Bilinen yapıya sahip
event.dataya eriş - Bitti
Asıl fark bug yüzeyinde görünür. Ortak zarf bütün bir parsing hatası kategorisini ortadan kaldırır; zorunlu id ve source ikilisi de her evente, tracing araçlarının servisler arasında takip edebileceği bir korelasyon anahtarı verir. time ise spesifikasyonda opsiyoneldir; timestamp’e ihtiyaç duyan bir consumer, eventi reddetmek yerine kendi saatini kullanmalı.
Sonuç#
Eventler bir servis, ekip veya protokol sınırını aştığında zarf doğru varsayılandır. Zorunlu attribute sayısı azdır, SDK kendi HTTP bağımlılığını getirmez ve geçiş yeni event tipleriyle başlar; mevcut producerlar şimdilik mevcut şekillerini korur.
Akışın tamamı tek bir servis ve tek bir ekip içinde kalıyorsa, üstelik eventleri ilişkilendirmesi gereken harici bir araç da yoksa bu varsayılandan sapabilirsin. Orada zarf yalnızca kimsenin okumadığı attributeları ekler. Karar sorusu şu: eventi, sana şeklini sormadan parse etmesi gereken biri olacak mı?
Uygulanabilir bir ilk adım: SDK’yı kur, tek bir reverse-DNS tip adı seç, zarfı tek bir producerdan emit et ve consumer tarafında HTTP binding ile parse et. Kapsamı oradan genişlet, taahhüt ettiğin tip adları için de bir registry tut.
Kaynaklar#
- CloudEvents Spesifikasyonu (yeni sekmede açılır) - CloudEvents standardını tanımlayan, temel spesifikasyon ve protokol bağlamalarını içeren resmi CNCF barındırmalı spesifikasyon deposu.
- CloudEvents JavaScript/TypeScript SDK (yeni sekmede açılır) - Node.js ve TypeScript uygulamalarında CloudEvents üretmek ve tüketmek için resmi SDK.
- CloudEvents Primer (yeni sekmede açılır) - CloudEvents spesifikasyonunun motivasyonunu, tasarım hedeflerini ve temel kavramlarını açıklayan giriş belgesi.
- Amazon EventBridge Belgeleri (yeni sekmede açılır) - CloudEvents’i serverless bileşenler arasında yönlendirmek için kullanılan olay veri yolu hizmetine yönelik resmi AWS kılavuzu.
- CloudEvents Resmi Web Sitesi (yeni sekmede açılır) - SDK’lara, spesifikasyon sürümlerine ve CNCF mezuniyet durumuna bağlantılar içeren proje ana sayfası.
- AWS Lambda En İyi Uygulamaları (yeni sekmede açılır) - Yapılandırılmış olayları tüketen ve yayan güvenilir Lambda olay işleyicileri oluşturmak için resmi rehber.
İlgili yazılar
Middy builder pattern, Zod validation, feature flags ve secrets management ile sürdürülebilir, type-safe Lambda middleware nasıl inşa edilir öğren.
lambda · middleware · typescript +7
AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Mimari kararlar, proje yapısı ve ölçekte ayakta kalan şema seçimleri.
aws-cdk · lambda · dynamodb +5
Bir Lambda filosu Middy'nin statik middleware modelini ne zaman aşar, projeye özel bir motor istek başına konfigürasyonu nasıl çözer, bakımı neye mal olur
lambda · middleware · performance +6
Middy'nin middleware kalıplarıyla Lambda geliştirmesini nasıl dönüştürdüğünü, tekrarlayan şablonlardan temiz, sürdürülebilir serverless fonksiyonlara geçişi keşfedin
lambda · middleware · serverless +5
CDK TypeScript Lambda için 9 bundler ve 3 cdk synth runner'ının ölçümlü karşılaştırması; her katman için varsayılan ve onu seçtiren kural.
aws-cdk · lambda · typescript +3