Skip to content

Pattern: EventBridge Event-Driven Pipeline

Decouple API request acceptance from async processing using emitEvent to publish to EventBridge, with SQS as a buffered target for Lambda consumers.

The Pattern

typescript
// src/lambdas/api/feedly/webhook.post.ts
import { emitEvent } from '@mantleframework/core'
import { defineApiHandler, z } from '@mantleframework/validation'
import type { DownloadRequestedDetail } from '#types/events'

const FeedlyWebhookRequestSchema = z.object({ articleURL: z.string() })

const api = defineApiHandler({
  auth: 'authorizer',
  schema: FeedlyWebhookRequestSchema,
  operationName: 'WebhookFeedly',
})

export const handler = api(async ({ context, userId, body, metadata }) => {
  // Publish event — EventBridge routes to DownloadQueue → StartFileUpload Lambda
  const eventDetail: DownloadRequestedDetail = {
    fileId,
    userId,
    sourceUrl: body.articleURL,
    correlationId: metadata.correlationId,
    requestedAt: new Date().toISOString(),
  }
  await emitEvent({ detailType: 'DownloadRequested', detail: eventDetail })

  return buildValidatedResponse(context, 202, { status: 'Accepted' }, ResponseSchema)
})
typescript
// src/lambdas/sqs/StartFileUpload/index.ts — the downstream consumer
import { emitEvent, defineSqsHandler } from '@mantleframework/core'
import type { DownloadCompletedDetail } from '#types/events'

const sqs = defineSqsHandler({
  operationName: 'StartFileUpload',
  parseBody: true,
  timeout: 900,
  memorySize: 2048,
  queue: 'DownloadQueue',
})

export const handler = sqs(async (record) => {
  // ... process download ...

  // Publish completion event for downstream observability
  const completedDetail: DownloadCompletedDetail = {
    fileId, correlationId, s3Key: fileName,
    fileSize: uploadResult.fileSize,
    completedAt: new Date().toISOString(),
  }
  await emitEvent({ detailType: 'DownloadCompleted', detail: completedDetail })
})

How It Works

The API Lambda accepts requests synchronously and immediately publishes a DownloadRequested event via emitEvent. EventBridge routes the event to an SQS queue using an inputTransformer (defined in mantle.config.ts) that reshapes the event detail into the queue message format. The SQS-triggered Lambda then processes the download asynchronously, publishing further events (DownloadCompleted, DownloadFailed) for observability. The API returns 202 Accepted without waiting for the download — decoupling request acceptance from potentially slow video processing.

Real-World Usage

Configuration

typescript
// mantle.config.ts
export default defineConfig({
  name: 'media-downloader',
  eventbridge: {
    bus: 'MediaDownloader',
    sqsTargets: [
      {
        detailType: 'DownloadRequested',
        queue: 'DownloadQueue',
        inputTransformer: {
          inputPaths: {
            fileId: '$.detail.fileId',
            sourceUrl: '$.detail.sourceUrl',
            correlationId: '$.detail.correlationId',
            userId: '$.detail.userId',
          },
          inputTemplate: `{
  "fileId": <fileId>,
  "sourceUrl": <sourceUrl>,
  "correlationId": <correlationId>,
  "userId": <userId>,
  "attempt": 1
}`,
        },
      },
    ],
  },
})

The sqsTargets array tells Mantle's infra generator to create an EventBridge rule that matches detailType: 'DownloadRequested' and forwards messages to the named SQS queue, applying the inputTransformer to reshape the event payload.

Variations

  • Direct Lambda target: Omit sqsTargets and use defineEventBridgeHandler on the consumer Lambda — EventBridge invokes it directly without buffering. Use when you don't need SQS retry/visibility-timeout semantics.
  • Fan-out: Add multiple sqsTargets for the same detailType to notify several consumers from one event.
  • Chained events: Each processing stage emits its own event (DownloadCompleted, DownloadFailed) so downstream services can react without polling.