-
Notifications
You must be signed in to change notification settings - Fork 0
Getting started
This page walks through a complete working Lambda deployment, once per trigger shape. All four end up importing the same createLambdaAdapter and exporting the same handler — only the Lambda trigger configuration differs.
- An AWS account with permissions to deploy Lambda + the relevant trigger (API Gateway, Function URL, or ALB).
- A DynamoDB table (or DynamoDB Local running at
http://localhost:8000for experimentation via the local bridges). - Node 20+ for local development.
- Your preferred IaC (SAM, CDK, Serverless Framework, Terraform, Pulumi, plain
awsCLI). This wiki shows config fragments as plain CloudFormation YAML — port to your tool of choice.
npm install dynamodb-toolkit-lambda dynamodb-toolkit @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodbNo framework peer dep. If you use TypeScript for handler authoring, also install @types/aws-lambda as a dev dependency — the adapter exports types that reference aws-lambda symbols but doesn't require them at runtime.
// src/handler.js
import {DynamoDBClient} from '@aws-sdk/client-dynamodb';
import {DynamoDBDocumentClient} from '@aws-sdk/lib-dynamodb';
import {Adapter} from 'dynamodb-toolkit';
import {createLambdaAdapter} from 'dynamodb-toolkit-lambda';
const ddb = DynamoDBDocumentClient.from(
new DynamoDBClient({region: process.env.AWS_REGION}),
{marshallOptions: {removeUndefinedValues: true}}
);
const planets = new Adapter({client: ddb, table: process.env.PLANETS_TABLE, keyFields: ['name']});
export const handler = createLambdaAdapter(planets, {mountPath: '/planets'});Everything outside handler runs once per container (cold start) and is reused across warm invocations — the DynamoDBDocumentClient, the Adapter, the policy merge, the factory. Keep the top of the module light: AWS SDK import + one client + one adapter is the fast path.
Modern default. Flat envelope, native cookies, cheapest per-request.
# template.yaml (SAM)
PlanetsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: src/handler.handler
Runtime: nodejs20.x
Environment:
Variables:
PLANETS_TABLE: !Ref PlanetsTable
Events:
AnyPlanets:
Type: HttpApi
Properties:
Path: /planets/{proxy+}
Method: ANY
RootPlanets:
Type: HttpApi
Properties:
Path: /planets
Method: ANYTwo events are needed because {proxy+} doesn't match the mount root /planets itself — only /planets/.... Both arrive at the same Lambda and the adapter's mountPath: '/planets' strips them consistently.
Same payload format as API Gateway HTTP; skip the API Gateway layer entirely.
PlanetsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handler.handler
Runtime: nodejs20.x
FunctionUrlConfig:
AuthType: AWS_IAM # or NONE for public; use a custom authorizer pattern otherwiseDrop mountPath when the function URL is the only thing routing to this Lambda — the adapter then owns / directly:
export const handler = createLambdaAdapter(planets); // no mountPath
// Function URL: https://abc123.lambda-url.us-east-1.on.aws/earthOlder API Gateway variant. Same factory, same export.
PlanetsApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
EndpointConfiguration: REGIONAL
PlanetsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handler.handler
Runtime: nodejs20.x
Events:
Any:
Type: Api
Properties:
RestApiId: !Ref PlanetsApi
Path: /planets/{proxy+}
Method: ANYThe adapter auto-detects the 1.0 payload (v1 events carry httpMethod + path, not version: '2.0') and emits APIGatewayProxyResult — the right envelope shape. See Event shapes.
Map the Lambda to an ALB listener rule. The adapter auto-detects ALB events (event.requestContext.elb marker).
PlanetsTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
TargetType: lambda
Targets:
- Id: !GetAtt PlanetsFunction.Arn
TargetGroupAttributes:
- Key: lambda.multi_value_headers.enabled
Value: 'true' # optional — see belowFlip lambda.multi_value_headers.enabled to true if downstream clients rely on duplicated headers (e.g. multiple Set-Cookie). The adapter mirrors whichever shape the trigger delivers — no extra configuration on the handler side. See Event shapes → Multi-value headers.
Assuming the API Gateway stage is at https://api.example.com/prod and the mount is /planets:
# Create
curl -X POST https://api.example.com/prod/planets/ \
-H 'content-type: application/json' \
-d '{"name":"earth","mass":5.97,"climate":"temperate"}'
# Read one
curl https://api.example.com/prod/planets/earth
# Partial update (PATCH merges, does not replace)
curl -X PATCH https://api.example.com/prod/planets/earth \
-H 'content-type: application/json' \
-d '{"population":8.2e9}'
# List with paging
curl 'https://api.example.com/prod/planets/?offset=0&limit=25'
# Projection
curl 'https://api.example.com/prod/planets/?fields=name,mass'
# Bulk fetch by name
curl 'https://api.example.com/prod/planets/-by-names?names=earth,mars,venus'
# Delete
curl -X DELETE https://api.example.com/prod/planets/plutoStage prefixes (/prod) are stripped by the trigger before the Lambda sees the event, so the adapter's mountPath shouldn't include them. See Options → mountPath.
Minimum DynamoDB permissions for the Lambda execution role:
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:BatchGetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:BatchWriteItem
- dynamodb:Query
- dynamodb:Scan
- dynamodb:TransactWriteItems
Resource:
- !GetAtt PlanetsTable.Arn
- !Sub '${PlanetsTable.Arn}/index/*'Prune by route — e.g. drop BatchWriteItem + DeleteItem if you never expose DELETE / or /-by-names.
If the Lambda owns every path under its trigger (typical for Function URLs and single-resource deployments), omit mountPath:
const handler = createLambdaAdapter(planets);
// GET /earth, POST /, PATCH /earth — all hit the adapter directly.The adapter matches routes against the full event path — event.path on v1 / ALB, event.rawPath on v2.
Don't round-trip to AWS while iterating. Spin up a local listener that speaks the exact Lambda event shape:
import http from 'node:http';
import {createNodeListener} from 'dynamodb-toolkit-lambda/local.js';
import {handler} from './src/handler.js';
http.createServer(createNodeListener(handler)).listen(3000);node local-server.js
curl http://localhost:3000/planets/earthBun / Deno / Cloudflare Workers variants and Koa / Express bridging live on Local debug bridges.
- Custom status codes, error bodies — Error handling.
- Composite (partition + sort) keys — Composite keys.
-
Per-tenant list filtering via
exampleFromContext— Options. - Understanding the four event shapes and their gotchas — Event shapes.