Skip to content

Quick start ​

A Lambda handling an SQS queue takes two files. Once that works, adding a second event source to the same Lambda is one more router, which is the part worth being here for.

Install ​

One package per event source your Lambda receives, plus base. base exports LambdaRouter, which every router plugs into. It is a peer dependency, so you install it yourself.

bash
npm install @lambda-event-router/base @lambda-event-router/sqs

A Lambda sitting behind an HTTP API and a queue adds one more:

bash
npm install @lambda-event-router/base @lambda-event-router/apigateway @lambda-event-router/sqs

If you are not sure which package you need, the packages page lists every AWS event source we cover.

Your first Lambda ​

1. Create a router and register a route ​

ts
import { createSQSRouter } from '@lambda-event-router/sqs'
import { z } from 'zod'

import { orders } from '../services/orders.js'

const ORDER_QUEUE_ARN = 'arn:aws:sqs:eu-west-2:123456789012:orders'

const OrderSchema = z.object({
  orderId: z.string(),
  quantity: z.number().int().positive(),
})

export const sqsRouter = createSQSRouter({ batchItemFailures: true })

sqsRouter.route({
  filters: {
    eventSourceArn: ORDER_QUEUE_ARN,
    messageAttributes: { type: 'OrderPlaced' },
  },
  bodySchema: OrderSchema,
  handler: async ({ body }) => {
    // body is typed from OrderSchema, with nothing to declare
    await orders.save({ orderId: body.orderId, quantity: body.quantity })
  },
})

Breaking that down:

  1. createSQSRouter makes a router for one event source. batchItemFailures: true reports failing records individually so only those get redelivered
  2. filters says which records this route takes, and every key has to match. This one only takes OrderPlaced messages off that queue, so a second route can filter on OrderCancelled and get its own handler. The keys come from the event source, and routing lists them
  3. bodySchema parses the message JSON and validates it, then types body from the same schema. See schema validation for which libraries work and what a failure does
  4. SQS hands Lambda a batch of records. The router unpacks it and calls your handler once per record, so ten messages arriving together run it ten times and you write for one message rather than a loop. Handlers covers everything else the request carries

2. Export the Lambda handler ​

ts
import type { Handler } from 'aws-lambda'
import { LambdaRouter } from '@lambda-event-router/base'

import { sqsRouter } from './routers/sqs.js'

const lambdaRouter = new LambdaRouter({ routers: [sqsRouter] })

export const handler: Handler = lambdaRouter.handler()

LambdaRouter is what AWS invokes. It works out which of its routers owns each event before any of your filters run, so nothing in your code has to sniff at the event shape.

Congratulations, that is a working Lambda. Point the queue at it and every message reaches your handler already parsed, validated and typed.

Multiple event sources ​

One LambdaRouter takes as many routers as you need, and each event reaches exactly one of them. Matching happens twice: LambdaRouter picks the router from the shape of the event, then that router picks the route from your filters.

  • A PUT /items/:itemId request goes to apiRouter, then to the route filtering on that method and path
  • A stream record with eventName: 'INSERT' goes to dynamoRouter, then to the route filtering on that event name and stream ARN

Adding the second router cannot change how the first one's events are routed, so the API behaves exactly as it did before the stream existed.

Open a file: api-handlers/putItem.ts | db-handlers/onItemInserted.ts | api.ts | dynamodb.ts | index.ts

File viewer
TSapi-handlers/putItem.ts

Both handlers above are plain functions with their request types named. Those types can be inferred from the route's filters and schemas instead when using defineRoute, which puts the route definition in the handler's file rather than the router's. See handlers for the same two handlers written that way.

ApiRequest takes the path params, then the query params, then the body, and this route sets no querySchema so its query is unknown. Returning the item on its own gives a 200 with the item as the body, and the response helpers cover the other status codes.

Where next ​

PageWhat it covers
RoutersWhat a router is, and what every one of them has in common
RoutingFilter keys, custom, match order and where schemas go
HandlersWhat a handler is given and what it may return
MiddlewareRunning code around your handlers
PackagesEvery router, with a page each