
AWS SDK for JavaScript v3: Node.js, Browser and S3 Guide
The AWS SDK for JavaScript v3 is AWS's official library for calling AWS from Node.js, browsers and Lambda. It is modular: you install one package per service, such as @aws-sdk/client-s3, create a client, and call client.send(new SomeCommand(input)). To start, run npm i @aws-sdk/client-s3 on Node.js 20 or later, sign in with aws login or aws sso login so the SDK finds credentials on its own, and send a PutObjectCommand. The old all-in-one aws-sdk (v2) package reached end of support on September 8, 2025, so all new code should use v3.
This guide covers installing the SDK in Node.js (ESM or CommonJS) and the browser, credentials on a laptop versus Lambda, ECS and EC2, the S3 calls you will actually use, other common services, retries and errors, and migrating from v2. Current as of October 2026.
Requirements
- Node.js 20 or newer. Per the aws-sdk-js-v3 README, v3.968.0 and later require Node.js 20+. The SDK follows the Node.js release schedule and drops a version about 8 months after it reaches end of life, so plan to be on Node.js 22 or 24 by early 2027.
- One package per service. Keep all
@aws-sdk/*packages on the same version so they share one copy of the core libraries. Releases ship almost every weekday. - TypeScript types are built in. The SDK is written in TypeScript; there is no
@typespackage to install. - Size. AWS's migration guide puts the full v2 package at about 40 MB, versus under 100 KB for one v3 client plus one command.
Step 1: Install and import
mkdir s3-demo && cd s3-demo
npm init -y
npm pkg set type=module
npm i @aws-sdk/client-s3
The packages ship both ES modules and CommonJS:
// ESM ("type": "module" or .mjs)
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
// CommonJS
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
Watch the names: the v3 S3 package is @aws-sdk/client-s3. aws-sdk/clients/s3 is the v2 import path, and @aws-sdk/s3 does not exist. S3 helpers are separate packages you add only if needed:
| Package | What it's for |
|---|---|
@aws-sdk/client-s3 |
All S3 commands (PutObjectCommand, GetObjectCommand, ...) |
@aws-sdk/lib-storage |
Upload helper for large files and streams (automatic multipart) |
@aws-sdk/s3-request-presigner |
getSignedUrl for pre-signed upload and download URLs |
@aws-sdk/credential-providers |
Explicit credential sources (fromIni, fromTemporaryCredentials, Cognito) |
Step 2: Credentials and region
Never put access keys in code. In Node.js, a client created without a credentials option uses the default credential provider chain, which stops at the first source that works: environment variables, IAM Identity Center (SSO), the shared ~/.aws/config and ~/.aws/credentials files, console login sessions, credential_process, web identity tokens, then ECS container credentials and EC2 instance metadata.
The same code therefore runs everywhere; only the environment changes:
| Where your code runs | Where credentials come from | What you configure |
|---|---|---|
| Your laptop | aws login or SSO session in ~/.aws |
aws login (AWS CLI 2.32.0+) or aws sso login, plus AWS_PROFILE if needed |
| AWS Lambda | The function's execution role | A least-privilege IAM role |
| ECS / Fargate | Task role via the container endpoint | taskRoleArn in the task definition |
| EC2 | Instance profile via instance metadata | An instance profile |
| EKS | IRSA web identity token or EKS Pod Identity | A service account linked to a role |
| GitHub Actions | OIDC web identity, then an assumed role | aws-actions/configure-aws-credentials |
aws login opens a browser and caches temporary credentials that the CLI and SDK refresh for sessions of up to 12 hours (CLI docs). The region comes from AWS_REGION (set automatically in Lambda), your profile, or the client option new S3Client({ region: 'eu-west-1' }).
When you need a specific source, import it explicitly:
import { fromIni, fromTemporaryCredentials } from '@aws-sdk/credential-providers';
const dev = new S3Client({ credentials: fromIni({ profile: 'dev' }) });
const prod = new S3Client({
credentials: fromTemporaryCredentials({
params: { RoleArn: 'arn:aws:iam::123456789012:role/ReadOnly', RoleSessionName: 'report-job' },
}),
});
If you see Could not load credentials from any providers or Region is missing, the chain found nothing: run aws sts get-caller-identity with the same profile and check that your session hasn't expired.
Step 3: S3 with @aws-sdk/client-s3
Create one client per region and credential set, at module scope, and reuse it; clients hold connection pools and cached credentials.
// s3.mjs
import {
S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand, NoSuchKey,
} from '@aws-sdk/client-s3';
const s3 = new S3Client({}); // region and credentials from the environment
const Bucket = process.env.BUCKET;
await s3.send(new PutObjectCommand({
Bucket, Key: 'notes/hello.txt', Body: 'Hello from AWS SDK v3', ContentType: 'text/plain',
}));
try {
const { Body } = await s3.send(new GetObjectCommand({ Bucket, Key: 'notes/hello.txt' }));
console.log(await Body.transformToString());
} catch (err) {
if (err instanceof NoSuchKey) console.log('Object not found');
else throw err;
}
const { Contents = [] } = await s3.send(new ListObjectsV2Command({ Bucket, Prefix: 'notes/' }));
for (const obj of Contents) console.log(obj.Key, obj.Size);
await s3.send(new DeleteObjectCommand({ Bucket, Key: 'notes/hello.txt' }));
Run it with BUCKET=your-bucket node s3.mjs. Three things trip people up coming from v2:
Bodyis a stream, not a Buffer. UsetransformToString(),transformToByteArray(), or pipe it to a file. Always consume or destroy it; unread bodies can exhaust the client's sockets under load.- Listing is paginated.
ListObjectsV2returns at most 1,000 keys. Use the built-in paginator instead of a token loop:
import { paginateListObjectsV2 } from '@aws-sdk/client-s3';
for await (const page of paginateListObjectsV2({ client: s3 }, { Bucket })) {
for (const obj of page.Contents ?? []) console.log(obj.Key);
}
- Errors are typed (more below).
Upload a large file
PutObjectCommand needs the whole body and is limited to 5 GB. For big files or streams, Upload from @aws-sdk/lib-storage does multipart uploads in parallel:
import { Upload } from '@aws-sdk/lib-storage';
import { createReadStream } from 'node:fs';
const upload = new Upload({
client: s3,
params: { Bucket, Key: 'videos/demo.mp4', Body: createReadStream('./demo.mp4') },
});
upload.on('httpUploadProgress', (p) => console.log(p.loaded, '/', p.total));
await upload.done();
Create a pre-signed URL
Pre-signed URLs let a browser download or upload one object without AWS credentials:
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const downloadUrl = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: 'reports/q3.pdf' }), { expiresIn: 900 });
const uploadUrl = await getSignedUrl(
s3, new PutObjectCommand({ Bucket, Key: 'uploads/avatar.png', ContentType: 'image/png' }), { expiresIn: 300 },
);
// Browser: fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': 'image/png' } })
The maximum is 7 days, and a URL stops working when the credentials that signed it expire, even if expiresIn is longer. getSignedUrl replaces v2's getSignedUrlPromise.
S3-compatible storage
The client also talks to Cloudflare R2, MinIO, Backblaze B2 and others via endpoint. Since v3.729.0 the SDK adds a CRC32 checksum to uploads by default (AWS docs), which some providers reject. If uploads fail with checksum or signature errors, set requestChecksumCalculation: 'WHEN_REQUIRED' and responseChecksumValidation: 'WHEN_REQUIRED' on the client, and check whether the provider needs forcePathStyle: true.
Step 4: Other services Node.js apps use most
Every client has the same shape. DynamoDB is easiest through the document client in @aws-sdk/lib-dynamodb, which converts plain objects to DynamoDB's typed attributes:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb';
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
await ddb.send(new PutCommand({ TableName: 'users', Item: { id: 'u1', name: 'Ada', plan: 'pro' } }));
const { Item } = await ddb.send(new GetCommand({ TableName: 'users', Key: { id: 'u1' } }));
An SQS consumer with long polling:
import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
const sqs = new SQSClient({});
const QueueUrl = process.env.QUEUE_URL;
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages: 10, WaitTimeSeconds: 20 }));
for (const msg of Messages) {
await handle(JSON.parse(msg.Body));
await sqs.send(new DeleteMessageCommand({ QueueUrl, ReceiptHandle: msg.ReceiptHandle })); // only after success
}
| Service | Package | Typical commands |
|---|---|---|
| S3 | @aws-sdk/client-s3 |
PutObjectCommand, GetObjectCommand |
| DynamoDB | @aws-sdk/client-dynamodb + @aws-sdk/lib-dynamodb |
PutCommand, GetCommand, QueryCommand |
| SQS | @aws-sdk/client-sqs |
SendMessageCommand, ReceiveMessageCommand |
| SNS | @aws-sdk/client-sns |
PublishCommand |
| SES | @aws-sdk/client-sesv2 |
SendEmailCommand |
| Secrets Manager | @aws-sdk/client-secrets-manager |
GetSecretValueCommand (cache the result) |
| Lambda | @aws-sdk/client-lambda |
InvokeCommand |
The v3 API reference lists every client and command.
Using the SDK in AWS Lambda
As of October 2026 Lambda's supported Node.js runtimes are nodejs22.x and nodejs24.x (Lambda runtimes). They include a specific minor version of SDK v3, not the latest, and it varies by runtime and region, so AWS recommends bundling the SDK packages you use (Lambda Node.js docs). Create clients outside the handler so warm invocations reuse them:
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({})); // once per container
export const handler = async (event) => {
const { Item } = await ddb.send(new GetCommand({ TableName: process.env.TABLE, Key: { id: event.pathParameters.id } }));
return { statusCode: Item ? 200 : 404, body: JSON.stringify(Item ?? { error: 'Not found' }) };
};
HTTP keep-alive is on by default in v3, so v2's AWS_NODEJS_CONNECTION_REUSE_ENABLED is no longer needed.
Retries, errors and timeouts
- Retries are built in. Clients retry throttling and transient errors with exponential backoff: mode
standard, 3 attempts by default (retry reference). Tune withmaxAttemptsandretryMode: 'adaptive'(client-side rate limiting, best for one hot resource) rather than wrapping calls in your own retry loop, which multiplies attempts during an outage. - Errors are classes. Catch with
instanceof(NoSuchKey, or the client'sS3ServiceExceptionbase class) and logerr.$metadata.httpStatusCodeanderr.$metadata.requestId, which AWS Support asks for. - Timeouts and sockets. The Node.js handler allows 50 concurrent sockets per client by default (maxSockets docs). Configure it per client, and use
abortSignalfor per-call deadlines:
import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({
requestHandler: { connectionTimeout: 2_000, requestTimeout: 10_000, httpsAgent: { maxSockets: 200 } },
});
await s3.send(new ListBucketsCommand({}), { abortSignal: AbortSignal.timeout(5000) });
Waiters (waitUntilObjectExists, waitUntilTableExists) replace sleep loops, and client.middlewareStack.add replaces v2's request event listeners for tracing headers or logging.
Using the SDK in the browser
v3 is tree-shakable, so bundlers include only the clients and commands you import. Never ship access keys to the browser: get temporary credentials for signed-in users from an Amazon Cognito identity pool (fromCognitoIdentityPool), or keep AWS calls on your server and hand the browser pre-signed URLs. For direct S3 calls, the bucket also needs a CORS rule allowing your origin.
Migrating from v2 (aws-sdk) to v3
v2 entered maintenance mode on September 8, 2024 and reached end of support on September 8, 2025 (AWS blog). It still runs but gets no fixes. AWS's codemod does most of the mechanical work:
npx aws-sdk-js-codemod@latest --dry --print -t v2-to-v3 src/ # preview
npx aws-sdk-js-codemod@latest -t v2-to-v3 src/
v2 (aws-sdk) |
v3 (@aws-sdk/*) |
|---|---|
require('aws-sdk'), every service |
One package per service |
new AWS.S3().getObject(params).promise() |
s3.send(new GetObjectCommand(params)) |
AWS.config.update({ region }) (global) |
new S3Client({ region }) per client |
Body is a Buffer |
Body is a stream |
s3.upload() |
Upload from @aws-sdk/lib-storage |
s3.getSignedUrl() (sync) |
getSignedUrl() from @aws-sdk/s3-request-presigner (async) |
AWS.DynamoDB.DocumentClient |
DynamoDBDocumentClient from @aws-sdk/lib-dynamodb |
Review the diff by hand; the full list of behavior changes is in UPGRADING.md.
Comparing clouds first? See our AWS vs Azure vs GCP pricing comparison, startup credits (AWS Activate included) on DevHunt perks, and DevOps tools that work alongside the SDK.
FAQ
What is @aws-sdk/client-s3?
The official Amazon S3 client of AWS SDK for JavaScript v3. It contains S3Client and one command class per S3 operation, such as PutObjectCommand, GetObjectCommand and ListObjectsV2Command, plus paginators and waiters.
What's the difference between aws-sdk and @aws-sdk?
aws-sdk is the v2 package: one large install with every service, past end of support. @aws-sdk/... is the v3 scope: one small package per service, built-in TypeScript types, and middleware. New code should use @aws-sdk/client-*.
Which Node.js version does the AWS SDK for JavaScript need?
Node.js 20 or newer for v3.968.0 and later. For new projects use Node.js 24 LTS; in Lambda, use nodejs22.x or nodejs24.x.
Does AWS SDK v3 work with TypeScript?
Yes. It is written in TypeScript and ships types for every client, command, input and output, so no extra packages are needed.
How do I use AWS credentials in Node.js without hard-coding keys?
Let the default chain find them: sign in locally with aws login or aws sso login, and on AWS give your Lambda function, ECS task or EC2 instance an IAM role. Create the client with no credentials option.