İçeriğe atla

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.

Ayhan Sipahi Ayhan Sipahi

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.

CloudEvent

Type'a göre filtrele

Source'a göre filtrele

Attribute'lara göre filtrele

Event Producer

Event Router

Lambda Handler 1

Lambda Handler 2

Lambda Handler 3

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:

Storage

Event Consumerlar

Event Processing

Event Producerlar

CloudEvents

CloudEvents

CloudEvents

Validated

Type Filter

Type Filter

Type Filter

API Gateway

Step Functions

External Webhooks

Lambda: Event Validator

EventBridge Bus

Lambda: User Service

Lambda: Notification Service

Lambda: Analytics Service

DynamoDB Event Store

S3 Event Archive

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:

  1. Dokümantasyon bulmak (genelde güncel değil)
  2. Custom event formatını anlamak
  3. O formata özel parsing logic yazmak
  4. O formatın izin verdiği edge caseleri test etmek

CloudEvents ile aynı iş şuna dönüşür:

  1. Event type ve source’u kontrol et
  2. Bilinen yapıya sahip event.dataya eriş
  3. 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#

İlgili yazılar