Published Sep 16, 2025 ⦁ Updated Oct 11, 2026 ⦁ 9 min read
AWS SDK for JavaScript v3: Node.js, Browser and S3 Guide

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 @types package 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:

  • Body is a stream, not a Buffer. Use transformToString(), 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. ListObjectsV2 returns 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 with maxAttempts and retryMode: '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's S3ServiceException base class) and log err.$metadata.httpStatusCode and err.$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 abortSignal for 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.