v2 still runs, but it no longer gets updates, including support for new services and features. v3 has been generally available since December 2020 and is a major rewrite: modular packages, a middleware stack, and TypeScript types. The good news is that service operation names and their input and output shapes are largely the same, so most changes are in how you create clients, call them and read results. This guide follows AWS's migration guide and the v3 UPGRADING notes, linked at the end.
What changed
- One package per service (
@aws-sdk/client-s3,@aws-sdk/client-dynamodb) instead of the singleaws-sdkpackage. - Operations are commands sent through a client:
await client.send(new GetObjectCommand(input)). .promise()is gone; calls return promises.- S3
GetObjectreturnsBodyas a stream, not a Buffer. Read it withawait Body.transformToString(). - No global
AWS.config: region and credentials are set on each client, and credential providers come from@aws-sdk/credential-providers. - Errors expose the error name and HTTP details through
error.nameanderror.$metadata. - Helpers moved to separate packages:
@aws-sdk/lib-storage,@aws-sdk/lib-dynamodb,@aws-sdk/s3-request-presigner.
Migration steps
Install only the clients you use
npm uninstall aws-sdk npm install @aws-sdk/client-s3 @aws-sdk/client-dynamodbAWS measures the full
aws-sdkpackage at about 40 MB, a single v3 client at about 3 MB, and a client plus one command at under 100 KB. Import from the package root, not from submodule paths inside it.Create clients and send commands
Before
const AWS = require("aws-sdk"); AWS.config.update({ region: "us-west-2" }); const ddb = new AWS.DynamoDB(); const data = await ddb.listTables({ Limit: 10 }).promise(); console.log(data.TableNames);After
import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb"; const ddb = new DynamoDBClient({ region: "us-west-2" }); const data = await ddb.send(new ListTablesCommand({ Limit: 10 })); console.log(data.TableNames);v3 also offers aggregated clients that keep the v2 method style:
new DynamoDB({ region })from the same package, thenawait ddb.listTables({ Limit: 10 })with no.promise(). That is a smaller diff but pulls in every command for the service. The client plus command style is what AWS recommends for bundle size.Read S3 object bodies as streams
Before
const obj = await s3.getObject({ Bucket, Key }).promise(); const text = obj.Body.toString("utf-8");After
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; const s3 = new S3Client({ region: "us-west-2" }); const obj = await s3.send(new GetObjectCommand({ Bucket, Key })); const text = await obj.Body?.transformToString();Set credentials per client
Before
AWS.config.update({ region: "eu-west-1", credentials: new AWS.SharedIniFileCredentials({ profile: "dev" }), }); const s3 = new AWS.S3();After
import { S3Client } from "@aws-sdk/client-s3"; import { fromIni } from "@aws-sdk/credential-providers"; const s3 = new S3Client({ region: "eu-west-1", credentials: fromIni({ profile: "dev" }), });If you set no credentials, v3 uses the default provider chain, in the same order as v2.
ChainableTemporaryCredentialsbecomesfromTemporaryCredentials({ params: { RoleArn } }). For local S3 endpoints such as LocalStack, the v2 options3ForcePathStyleisforcePathStylein v3, next toendpoint.Update error handling
Before
try { await s3.getObject({ Bucket, Key }).promise(); } catch (err) { if (err.code === "NoSuchKey") return null; console.error(err.statusCode); throw err; }After
try { await s3.send(new GetObjectCommand({ Bucket, Key })); } catch (err) { if (err.name === "NoSuchKey") return null; console.error(err.$metadata?.httpStatusCode); throw err; }Service exceptions also support
instanceofchecks against the classes each client exports. The raw response is onerr.$response.Replace the high-level helpers
s3.upload()andManagedUpload: useUploadfrom@aws-sdk/lib-storage.s3.getSignedUrl()andgetSignedUrlPromise(): usegetSignedUrl(client, command, { expiresIn })from@aws-sdk/s3-request-presigner.AWS.DynamoDB.DocumentClient: wrap a client withDynamoDBDocumentClient.from(client)from@aws-sdk/lib-dynamodb. v2 droppedundefinedvalues for you; in v3 setremoveUndefinedValues: trueinmarshallOptionsto keep that behavior.- Waiters: import the one you need, for example
await waitUntilBucketExists({ client, maxWaitTime: 60 }, { Bucket }).
How to find every affected call
Start by finding every import of v2, then every call style that changed. These ast-grep patterns match the syntax tree, so multi-line calls are found too:
sg run --pattern 'require("aws-sdk")' --lang js
sg run --pattern 'import $AWS from "aws-sdk"' --lang ts
sg run --pattern 'new AWS.$SERVICE($$$ARGS)' --lang ts
sg run --pattern '$CALL.promise()' --lang ts
sg run --pattern '$OBJ.Body.toString($$$ARGS)' --lang ts
sg run --pattern '$ERR.code === $CODE' --lang tsRepeat with --lang js and --lang tsx as needed, and look for submodule imports such as aws-sdk/clients/s3. $CALL.promise() also matches non-AWS code that happens to have a .promise() method, and $ERR.code matches any error object, so review each hit. On Linux, sg may be a system command; ast-grep run is the same tool.
Testing the change
- Rewrite mocks. Stubs on
AWS.S3.prototype.getObjector helpers built for v2 do not intercept v3 calls. Mocksendon the client, or use a v3-aware mocking library such as aws-sdk-client-mock. - Mocked S3 bodies must now be streams with
transformToString(). A test that returns a Buffer will pass in a way production never will. - Run integration tests against a real account or LocalStack, with
forcePathStyleset for local endpoints. - Check bundle size and cold start if you deploy to Lambda; the modular clients should make both smaller.
Automating it
AWS maintains an official codemod, aws-sdk-js-codemod. Preview with npx aws-sdk-js-codemod@latest --dry --print -t v2-to-v3 PATH..., then run without --dry --print to apply. It covers common cases such as imports, client creation and removing .promise(), and AWS asks you to review the changes thoroughly before deploying. Use it: it is free and fast.
What it does not do is prove the result. Versyka finds the affected call sites with ast-grep, rewrites them along with the code around them (body handling, error checks, mocks), and runs your repository's own install, typecheck, build and tests in a disposable sandbox before opening a pull request. If the checks fail, no pull request is opened.
Was this guide helpful?
Check your own repository
Paste your package.json, requirements.txt or pyproject.toml into the free upgrade check to see which API SDKs are behind, with no account. Or let Versyka run this migration on one repository and open a pull request that has already passed your tests.