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 matcher, which is the part worth being here for.

Install ​

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

bash
npm install @latcher/base @latcher/sqs

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

bash
npm install @latcher/base @latcher/apigateway @latcher/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 matcher and register a route ​

ts
import { createSQSMatcher } from '@latcher/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 sqsMatcher = createSQSMatcher({ batchItemFailures: true })

sqsMatcher.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. createSQSMatcher makes a matcher 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 matcher 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 { Latcher } from '@latcher/base'

import { sqsMatcher } from './matchers/sqs.js'

const latcher = new Latcher({ matchers: [sqsMatcher] })

export const handler: Handler = latcher.handler()

Latcher is what AWS invokes. It works out which of its matchers 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 Latcher takes as many matchers as you need, and each event reaches exactly one of them. Matching happens twice: Latcher picks the matcher from the shape of the event, then that matcher picks the route from your filters.

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

Adding the second matcher 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 matcher'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
MatchersWhat a matcher 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 matcher, with a page each