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.
npm install @latcher/base @latcher/sqsA Lambda sitting behind an HTTP API and a queue adds one more:
npm install @latcher/base @latcher/apigateway @latcher/sqsIf 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
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:
createSQSMatchermakes a matcher for one event source.batchItemFailures: truereports failing records individually so only those get redeliveredfilterssays which records this route takes, and every key has to match. This one only takesOrderPlacedmessages off that queue, so a second route can filter onOrderCancelledand get its own handler. The keys come from the event source, and routing lists thembodySchemaparses the message JSON and validates it, then typesbodyfrom the same schema. See schema validation for which libraries work and what a failure does- 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
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/:itemIdrequest goes toapiMatcher, then to the route filtering on that method and path - A stream record with
eventName: 'INSERT'goes todynamoMatcher, 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
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
| Page | What it covers |
|---|---|
| Matchers | What a matcher is, and what every one of them has in common |
| Routing | Filter keys, custom, match order and where schemas go |
| Handlers | What a handler is given and what it may return |
| Middleware | Running code around your handlers |
| Packages | Every matcher, with a page each |