@aws-sdk v3 Modular vs v2 Migration Guide 2026
@aws-sdk v3 Modular vs v2 Migration Guide 2026
TL;DR
AWS SDK for JavaScript v2 (aws-sdk) is in maintenance mode — it still works but receives only critical security fixes, not new features. @aws-sdk v3 (launched 2020, reached feature parity in 2022) is the current version and the only choice for new projects in 2026. The key differences are modular package architecture (import only what you use), a middleware-based plugin system, improved TypeScript types, and dramatically smaller bundle sizes for Lambda and edge deployments. Migration from v2 is mostly mechanical but requires updating import paths, instantiation patterns, and pagination utilities.
Key Takeaways
- @aws-sdk/client-s3: 1.2M weekly downloads (v3), vs
aws-sdkv2 at 6M (still dominant in legacy codebases) - Bundle size: importing only S3 from v3 — ~95KB gzip vs ~900KB for the entire v2 SDK
- Tree-shaking: v3 packages are ES modules; Lambda cold starts drop 40-70% for services that previously imported the full v2 SDK
- Middleware stack: v3 uses a composable middleware chain (like Express) — add retry logic, signing, logging without forking the SDK
- TypeScript: v3 generates types from service API definitions — exact input/output types per operation, no more
any - Pagination: v3 has built-in paginator helpers (
paginateListObjects) that eliminate manual NextToken/Marker loops
Why v2 Still Has More Downloads
aws-sdk v2 has 6M+ weekly downloads in 2026, despite v3 being the official recommendation. The reasons are instructive:
- Lambda execution environments — AWS Lambda included
aws-sdkv2 in the Node.js runtime until Node.js 18. Any Lambda not bundling its own SDK uses v2 automatically. - Legacy codebases — Enterprise Node.js apps started before 2020 often have thousands of
require('aws-sdk')calls. - CDK and tooling — Some infrastructure-as-code tools internally depended on v2.
- Gradual migration — Many teams are mid-migration, still using v2 in parts of their codebase.
AWS deprecated v2 in September 2024, with end of support (no security fixes) planned for September 2025. If you're on v2 in 2026, you're on unsupported software.
Architecture Differences
v2: Single Monolithic Package
// v2: one import, everything available
import AWS from 'aws-sdk'
// Instantiate any service
const s3 = new AWS.S3({ region: 'us-east-1' })
const dynamodb = new AWS.DynamoDB({ region: 'us-east-1' })
const lambda = new AWS.Lambda({ region: 'us-east-1' })
Installing aws-sdk pulls in ~100MB of code covering every AWS service. For a Lambda using only S3, you import the full SDK and pay the cold start penalty for every unused service.
v3: Modular Service Packages
// v3: import only what you need
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'
const s3 = new S3Client({ region: 'us-east-1' })
const ddb = new DynamoDBClient({ region: 'us-east-1' })
Each AWS service is its own npm package. Adding S3 doesn't add Lambda, DynamoDB, or any other service. A Lambda function that only uses S3 installs ~3MB instead of ~100MB.
Migration: Common Patterns
S3
// ===== v2 =====
import AWS from 'aws-sdk'
const s3 = new AWS.S3()
// Get object
const result = await s3.getObject({
Bucket: 'my-bucket',
Key: 'my-key',
}).promise()
const body = result.Body?.toString()
// Put object
await s3.putObject({
Bucket: 'my-bucket',
Key: 'my-key',
Body: 'hello world',
ContentType: 'text/plain',
}).promise()
// ===== v3 =====
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const s3 = new S3Client({ region: 'us-east-1' })
// Get object
const result = await s3.send(new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'my-key',
}))
// Body is now a ReadableStream — convert appropriately:
const body = await result.Body?.transformToString()
// Put object
await s3.send(new PutObjectCommand({
Bucket: 'my-bucket',
Key: 'my-key',
Body: 'hello world',
ContentType: 'text/plain',
}))
// Presigned URL (separate package in v3)
const url = await getSignedUrl(s3, new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'my-key',
}), { expiresIn: 3600 })
The key v3 changes for S3:
.promise()is gone — all operations return Promises nativelyBodyis aReadableStream | Blob | string, not aBuffer— use.transformToString(),.transformToByteArray(), or pipe it- Presigned URLs require
@aws-sdk/s3-request-presigneras a separate install
DynamoDB
// ===== v2 =====
const docClient = new AWS.DynamoDB.DocumentClient()
const result = await docClient.get({
TableName: 'Users',
Key: { userId: '123' },
}).promise()
const user = result.Item
// ===== v3: DynamoDB DocumentClient =====
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb'
const client = new DynamoDBClient({ region: 'us-east-1' })
const docClient = DynamoDBDocumentClient.from(client, {
marshallOptions: { removeUndefinedValues: true },
})
// Get
const result = await docClient.send(new GetCommand({
TableName: 'Users',
Key: { userId: '123' },
}))
const user = result.Item // typed, no manual unmarshalling
// Put
await docClient.send(new PutCommand({
TableName: 'Users',
Item: { userId: '123', name: 'Alice', createdAt: new Date().toISOString() },
}))
// Query with expression
const queryResult = await docClient.send(new QueryCommand({
TableName: 'Users',
IndexName: 'email-index',
KeyConditionExpression: 'email = :email',
ExpressionAttributeValues: { ':email': 'alice@example.com' },
}))
const users = queryResult.Items ?? []
The @aws-sdk/lib-dynamodb package provides the DocumentClient equivalent — it handles marshalling/unmarshalling JavaScript values to DynamoDB's typed format. Install it alongside @aws-sdk/client-dynamodb.
SES (Email)
// v2
await ses.sendEmail({
Source: 'noreply@example.com',
Destination: { ToAddresses: ['user@example.com'] },
Message: {
Subject: { Data: 'Hello' },
Body: { Text: { Data: 'World' } },
},
}).promise()
// v3 — use SESv2 for new features (templates, contacts)
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'
const ses = new SESv2Client({ region: 'us-east-1' })
await ses.send(new SendEmailCommand({
FromEmailAddress: 'noreply@example.com',
Destination: { ToAddresses: ['user@example.com'] },
Content: {
Simple: {
Subject: { Data: 'Hello' },
Body: { Text: { Data: 'World' } },
},
},
}))
Pagination: The Biggest API Improvement
v2 pagination required manual NextToken loops. v3 provides paginator functions:
// ===== v2: manual pagination =====
let continuationToken: string | undefined
const allObjects: AWS.S3.Object[] = []
do {
const result = await s3.listObjectsV2({
Bucket: 'my-bucket',
ContinuationToken: continuationToken,
}).promise()
allObjects.push(...(result.Contents ?? []))
continuationToken = result.NextContinuationToken
} while (continuationToken)
// ===== v3: built-in paginator =====
import { S3Client, paginateListObjectsV2 } from '@aws-sdk/client-s3'
const s3 = new S3Client({ region: 'us-east-1' })
const allObjects = []
for await (const page of paginateListObjectsV2({ client: s3 }, { Bucket: 'my-bucket' })) {
allObjects.push(...(page.Contents ?? []))
}
// Automatic pagination — no token management needed
Every AWS service with paginated responses has a corresponding paginate* function in v3.
Middleware Stack
v3's middleware system is the most powerful architectural improvement:
import { S3Client } from '@aws-sdk/client-s3'
const s3 = new S3Client({ region: 'us-east-1' })
// Add logging middleware
s3.middlewareStack.add(
(next, context) => async (args) => {
console.log(`[AWS] ${context.commandName} started`)
const start = Date.now()
const result = await next(args)
console.log(`[AWS] ${context.commandName} completed in ${Date.now() - start}ms`)
return result
},
{
step: 'initialize',
name: 'loggingMiddleware',
}
)
// Add custom retry logic middleware
s3.middlewareStack.add(
(next) => async (args) => {
let attempt = 0
while (true) {
try {
return await next(args)
} catch (err) {
if (attempt++ >= 3) throw err
await new Promise(resolve => setTimeout(resolve, 2 ** attempt * 100))
}
}
},
{ step: 'finalizeRequest', name: 'retryMiddleware' }
)
The middleware stack replaces v2's event hooks with a composable, typed system. Libraries can export middleware plugins that users opt into.
TypeScript Improvements
v3 generates TypeScript types directly from AWS service API definitions:
import { PutItemCommandInput } from '@aws-sdk/client-dynamodb'
// ✅ Fully typed — TypeScript knows all valid fields and their types
const input: PutItemCommandInput = {
TableName: 'Users',
Item: {
userId: { S: '123' }, // 'S' for string
count: { N: '42' }, // 'N' for number (as string)
active: { BOOL: true }, // 'BOOL' for boolean
},
ConditionExpression: 'attribute_not_exists(userId)',
}
In v2, most inputs and outputs were typed as any or with loose types. v3 gives precise types per operation, including response shapes — enabling autocompletion and catching type errors at compile time.
Bundle Size Impact
For a Lambda function using only S3 and DynamoDB:
| Scenario | Installed Size | Gzip |
|---|---|---|
v2 full SDK (aws-sdk) | ~100MB | ~9MB |
| v3 S3 + DynamoDB only | ~8MB | ~850KB |
| v3 S3 only (bundled, tree-shaken) | ~3MB | ~300KB |
| v2 on Lambda (built-in, not bundled) | 0 (built-in) | 0 |
The "0" row for Lambda built-in is why many teams haven't migrated — Lambda included aws-sdk v2 in the execution environment. Since Node 18 (now the minimum recommended runtime), AWS no longer includes v2 by default. Bundling v3 is now both required and recommended.
When using esbuild or tsup with proper tree-shaking, a Lambda using only @aws-sdk/client-s3 bundles to ~300KB gzip — a 30x reduction from the full v2 SDK.
Error Handling
v3 improves error handling with typed error classes per service:
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
import { NoSuchKey, S3ServiceException } from '@aws-sdk/client-s3'
const s3 = new S3Client({ region: 'us-east-1' })
try {
const result = await s3.send(new GetObjectCommand({
Bucket: 'my-bucket',
Key: 'missing-file.txt',
}))
} catch (err) {
if (err instanceof NoSuchKey) {
// Typed: this is specifically a 404 NoSuchKey error
console.log('File does not exist')
} else if (err instanceof S3ServiceException) {
// Any other S3 service error
console.log(`S3 error: ${err.name} — ${err.message}`)
console.log(`HTTP status: ${err.$response?.statusCode}`)
} else {
// Network error, timeout, etc.
throw err
}
}
In v2, error handling required checking err.code string values (err.code === 'NoSuchKey'). v3 gives you instanceof checks with TypeScript-aware typed error classes, one per AWS error code.
Credentials Configuration
// v3 credential providers
import { S3Client } from '@aws-sdk/client-s3'
import { fromEnv } from '@aws-sdk/credential-providers'
import { fromIni } from '@aws-sdk/credential-providers'
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'
// Explicit env vars
const s3 = new S3Client({
region: 'us-east-1',
credentials: fromEnv(), // AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
})
// Assume a role (common in CI and cross-account access)
const crossAccountS3 = new S3Client({
region: 'us-east-1',
credentials: fromTemporaryCredentials({
params: {
RoleArn: 'arn:aws:iam::123456789:role/MyRole',
RoleSessionName: 'my-session',
},
}),
})
// Named profile from ~/.aws/credentials
const profileS3 = new S3Client({
region: 'us-east-1',
credentials: fromIni({ profile: 'staging' }),
})
The @aws-sdk/credential-providers package consolidates all credential sources — environment variables, IAM roles, SSO, web identity tokens, and more. The default credential chain (no credentials option) checks environment → ECS task role → EC2 instance metadata, which works for most production deployments.
Migration Checklist
When migrating a project from v2 to v3:
[ ] Replace aws-sdk with individual @aws-sdk/client-* packages
[ ] Update all instantiation: new AWS.S3() → new S3Client()
[ ] Remove .promise() calls — v3 returns Promises natively
[ ] Update S3 Body handling: Buffer → transformToString() / transformToByteArray()
[ ] Install @aws-sdk/lib-dynamodb for DocumentClient equivalent
[ ] Replace manual pagination loops with paginate* helpers
[ ] Update presigned URL code to @aws-sdk/s3-request-presigner
[ ] Update credential configuration (CredentialProvider changed)
[ ] Test in Lambda with Node 18+ (no longer has built-in v2)
[ ] Enable bundling (esbuild/tsup) for Lambda to get tree-shaking benefits
Methodology
- npm download data from npmjs.com registry API, March 2026
- @aws-sdk v3 docs: docs.aws.amazon.com/AWSJavaScriptSDK/v3
- aws-sdk v2 end of support announcement: aws.amazon.com/blogs/developer/announcing-end-of-support-for-aws-sdk-for-javascript-v2
- Bundle sizes measured via bundlephobia.com and local Lambda build analysis
Compare AWS SDK with other cloud SDKs on PkgPulse.
Related: drizzle-seed vs @snaplet/seed vs Prisma Seed 2026 · Hono RPC vs tRPC vs ts-rest Type-Safe API Clients 2026