@aws-sdk v3
Use modular AWS SDK v3 clients in Node.js Lambdas and containers - import only what you need, configure middleware once, reuse clients across invocations.
Recipe
Quick-reference recipe card - copy-paste ready.
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: process.env.AWS_REGION });
export async function getObjectText(bucket: string, key: string) {
const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
return await res.Body?.transformToString();
}When to reach for this: Any AWS service call from Node 24 or Lambda nodejs24.x. Do not add aws-sdk v2.
Working Example
// src/aws/clients.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb";
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
import { NodeHttpHandler } from "@smithy/node-http-handler";
const requestHandler = new NodeHttpHandler({
connectionTimeout: 3_000,
requestTimeout: 10_000,
});
const ddbDoc = DynamoDBDocumentClient.from(
new DynamoDBClient({ maxAttempts: 3, requestHandler }),
{ marshallOptions: { removeUndefinedValues: true } }
);
const ssm = new SSMClient({ maxAttempts: 3 });
export async function getOrder(id: string) {
const res = await ddbDoc.send(
new GetCommand({ TableName: process.env.TABLE_NAME!, Key: { id } })
);
return res.Item;
}
export async function putOrder(item: Record<string, unknown>) {
await ddbDoc.send(
new PutCommand({ TableName: process.env.TABLE_NAME!, Item: item })
);
}
let cachedApiKey: string | undefined;
export async function getApiKey() {
if (cachedApiKey) return cachedApiKey;
const res = await ssm.send(
new GetParameterCommand({ Name: "/prod/api/KEY", WithDecryption: true })
);
cachedApiKey = res.Parameter?.Value;
return cachedApiKey!;
}// src/handler.ts
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { getOrder } from "./aws/clients.js";
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const id = event.pathParameters?.id!;
const order = await getOrder(id);
return {
statusCode: order ? 200 : 404,
body: JSON.stringify(order ?? { error: "not found" }),
};
};What this demonstrates:
- Separate packages per service (
@aws-sdk/client-dynamodb,@aws-sdk/client-ssm) - Document client for plain JS attribute maps
- Shared
NodeHttpHandlertimeouts and init-scoped client reuse - SSM parameter cached in module scope across warm invocations
Deep Dive
v2 vs v3
| Aspect | AWS SDK v2 | AWS SDK v3 |
|---|---|---|
| Import | import AWS from "aws-sdk" | @aws-sdk/client-s3 |
| Bundle size | Entire SDK | Per-service tree-shakeable |
| API | .promise() | client.send(new Command()) |
| Middleware | Limited | Smithy middleware stack |
Command Pattern
Every operation is a command class:
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.QUEUE_URL!,
MessageBody: JSON.stringify({ orderId: "42" }),
}));Commands are immutable; safe to construct per request.
Middleware Example
import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
s3.middlewareStack.add(
(next) => async (args) => {
const start = Date.now();
const result = await next(args);
console.log(JSON.stringify({ awsCall: args.request?.hostname, ms: Date.now() - start }));
return result;
},
{ step: "finalizeRequest", name: "logLatency" }
);Use middleware for cross-cutting logging, not business logic.
Lambda Runtime Bundling
Lambda Node.js 18+ runtimes include AWS SDK for JavaScript v3. You can mark @aws-sdk/* as external in esbuild to shrink deployment packages. Pin SDK versions in CI if you rely on runtime-bundled SDK behavior.
Credential Chain
On Lambda, credentials come from the execution role automatically. Local dev uses shared credentials file or SSO:
aws sso login --profile dev
AWS_PROFILE=dev npm run invoke:localGotchas
- Importing
@aws-sdk/client-s3and entirelib-dynamodbunused - still better than v2, but audit imports. Fix: one client module per domain. - New client per request - TLS handshake overhead. Fix: module-scope singleton.
- Missing
maxAttemptson flaky networks - transient 503s fail invocations. Fix:maxAttempts: 3default is often enough; tune per service. - Large
GetObjectinto memory - OOM on big files. Fix: streamBodyto S3 upload or disk. - SSM parameter fetch every invoke - slow and throttled. Fix: cache with TTL in module scope.
- Region mismatch -
PermanentRedirecton S3. Fix: setregionon client orAWS_REGIONenv.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| @aws-sdk v3 modular | Default for all AWS calls | Never for new code |
| AWS SDK v2 | Legacy maintenance only | New Lambdas |
| AWS Data API | Aurora Serverless SQL without VPC | Need full PG features |
| AWS CDK L2 constructs | Infra provisioning | Application-level S3/GetObject calls |
FAQs
Do we need to zip node_modules/@aws-sdk?
For Lambda, you can externalize SDK if using a managed runtime that includes it. For containers, install only the clients you need in the image.
How do I paginate?
Use paginator helpers: import { paginateListObjectsV2 } from "@aws-sdk/client-s3" or loop on NextToken in commands.
Does v3 work with TypeScript strict mode?
Yes. Command input types are generated from Smithy models.
What about @aws-sdk/client-sts AssumeRole?
Create an STS client, assume role, pass returned credentials to service clients via credentials config for cross-account access.
How does this relate to Secrets Manager?
Same pattern: @aws-sdk/client-secrets-manager + GetSecretValueCommand, cached at init. See Secrets Managers.
Can I use v3 in Express on ECS?
Identical code paths. IAM task role replaces Lambda execution role for credentials.
Related
- Lambda Handler Patterns - client init scope
- Cold Start Mitigation - external SDK in bundles
- Secrets Managers - SSM and Secrets Manager
- Serverless Basics - Lambda setup
- Serverless Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.