New to Rust? Grab our free Rust for Beginners eBook Get it free →
Getting Started with AWS SDK and Node.js: Interacting with AWS Services

At the terminal, `node upload.mjs sample.txt` looks like one action, but the script has to turn that file into an AWS request. I was surprised that the request’s shape matters just as much as the file being sent.
You’ll be able to connect the command you run to the service operation your code expresses. The AWS SDK for JavaScript is how Node.js communicates that operation to AWS.
What the AWS SDK for JavaScript does in Node.js
The AWS SDK for JavaScript v3 is a set of service-specific packages that lets a Node.js app call AWS services. For S3, a client holds shared configuration, a command describes one operation, and client.send sends that command and returns the service response.
The client keeps Region and credentials in its configuration, while the command names the operation and carries the bucket, object name and data. AWS publishes one package per service.
| SDK piece | What it controls |
|---|---|
| S3Client | Region, credential lookup and S3 requests |
| PutObjectCommand | Bucket, object key and file bytes for one upload |
| client.send(command) | Sends the operation and resolves with the response |
What you need before the first S3 request
Use an Active LTS release of Node.js and npm. The AWS getting-started guide recommends Active LTS for development, and the current S3 client package declares support for Node.js 20 and later.
- An S3 bucket that already exists.
- The AWS Region where that bucket lives.
- An AWS identity with permission to add an object to that bucket.
For local development, AWS recommends IAM Identity Center if your organization has not chosen another sign-in method. Node.js can read the resulting profile through its default credential provider chain, while AWS compute should use a role attached to the workload.
Set AWS_REGION to the bucket’s Region and S3_BUCKET to its name before running the sample.
The identity also needs s3:PutObject, because credentials identify the caller but do not grant permission to write to the bucket. See AWS’s Node.js credential guide and IAM Identity Center setup.
Upload one file with the S3 client
The sample requires an existing bucket and a local file. It sends one PutObject request and leaves bucket creation to AWS setup.
Step 1: Install the S3 service package
Run the commands below to create a package and add the S3 client.
npm init -y
npm install @aws-sdk/client-s3
Step 2: Let Node.js resolve AWS credentials
The client receives a Region but no access keys, because Node.js resolves credentials from configured providers. Stale environment credentials can override shared configuration.
Step 3: Send a PutObject command
Save this as upload.mjs. The function accepts an S3 client so the same file-reading and command-building path can be tested without contacting AWS.
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
export async function uploadFile(client, bucket, filePath) {
if (!bucket) throw new Error("Set S3_BUCKET to an existing bucket name");
if (!filePath) throw new Error("Pass a local file path");
const key = basename(filePath);
const body = await readFile(filePath);
const result = await client.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
}));
return { bucket, key, statusCode: result.$metadata?.httpStatusCode };
}
export async function main({ client, bucket = process.env.S3_BUCKET, filePath = process.argv[2], region = process.env.AWS_REGION } = {}) {
if (!bucket) throw new Error("Set S3_BUCKET to an existing bucket name");
if (!filePath) throw new Error("Pass a local file path");
if (!client && !region) throw new Error("Set AWS_REGION to the bucket's Region");
const s3 = client ?? new S3Client({ region });
return uploadFile(s3, bucket, filePath);
}
if (process.argv[1]?.endsWith("upload.mjs")) {
main()
.then(({ bucket, key }) => console.log(`Uploaded s3://${bucket}/${key}`))
.catch((error) => {
console.error(error.name, error.message);
process.exitCode = 1;
});
}
The command’s Bucket selects the S3 destination, Key names its object and Body contains the file bytes. basename defaults the key to the local filename.
S3 matches an object by bucket and Key, so a write to an existing Key replaces its current contents in an unversioned bucket, while a versioned bucket stores another version. Choose a unique Key when replacement is not intended, or use S3’s conditional upload support.
Save this as test/upload.test.mjs beside the sample.txt fixture. I kept the client injectable so a fake send method can inspect the SDK command without AWS credentials.
Node SDK test payload from local fixture.
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { main, uploadFile } from "../upload.mjs";
const fixture = new URL("../sample.txt", import.meta.url);
test("uploads file bytes under its base name with one SDK command", async () => {
let received;
const client = {
async send(command) {
received = command;
return { $metadata: { httpStatusCode: 200 } };
},
};
const result = await uploadFile(client, "example-bucket", fixture.pathname);
assert.ok(received instanceof PutObjectCommand);
assert.equal(received.input.Bucket, "example-bucket");
assert.equal(received.input.Key, "sample.txt");
assert.deepEqual(received.input.Body, await readFile(fixture));
assert.deepEqual(result, {
bucket: "example-bucket",
key: "sample.txt",
statusCode: 200,
});
});
test("rejects a missing bucket before sending a request", async () => {
let sent = false;
const client = { async send() { sent = true; } };
await assert.rejects(uploadFile(client, "", fixture.pathname), /S3_BUCKET/);
assert.equal(sent, false);
});
test("main sends the file through its injected client", async () => {
let received;
const client = {
async send(command) {
received = command;
return { $metadata: { httpStatusCode: 200 } };
},
};
const result = await main({
client,
bucket: "example-bucket",
filePath: fixture.pathname,
});
assert.ok(received instanceof PutObjectCommand);
assert.equal(result.statusCode, 200);
});
The fake client accepts a PutObjectCommand and returns fixture metadata, so these assertions cover command inputs and the missing-bucket guard without claiming an S3 response. That 200 status comes from the fake client, not Amazon S3.
Diagnose missing credentials and denied uploads
AWS credential lookup can fail before S3 receives a request. Confirm which identity the SDK selected before changing the command.
| Symptom | What to check | Why it matters |
|---|---|---|
| The SDK cannot find credentials | Sign in to the intended profile, then check for stale environment credentials. | The provider chain needs an identity before it can sign a request. |
| S3 returns AccessDenied | Check identity and bucket policies for s3:PutObject. | Valid credentials do not grant permission to write an object. |
| The bucket is not found or the request has a Region error | Check S3_BUCKET and match AWS_REGION to the bucket’s Region. | The bucket name and endpoint Region select the destination. |
| The local script says to set S3_BUCKET | Set the bucket name before running upload.mjs. | The guard stops before constructing an S3 client or sending a request. |
Run the sample without S3_BUCKET to exercise the guard. It stops before constructing the S3 client, so this output is local configuration feedback, not an S3 error.
node upload.mjs sample.txt

If the bucket exists and the Region matches but S3 returns AccessDenied, compare the selected role’s policy with the bucket policy. A basic object upload needs s3:PutObject, while ACL or encryption options can require extra permissions, so this sample leaves them out.
Keep the first AWS request narrow
Run the local tests before using the same script with an AWS profile and bucket. The tests verify the command and its inputs, but they cannot verify IAM authorization or an S3 response.
node --test --test-reporter=tap

PutObject is a single-request upload, capped at 5 GB. For a larger file or a stream, use the AWS SDK’s multipart upload helper instead of expanding this first example.
AWS SDK for Node.js questions
Node.js and browser applications use different credential setups, and larger S3 objects need a different upload path.
Can browser JavaScript use the Node.js credential chain?
No. Browser code needs a browser-specific identity provider such as Amazon Cognito Identity. The Node.js provider chain in this example is for server-side JavaScript.
How should I upload a file larger than 5 GB?
Use multipart upload. The AWS SDK for JavaScript provides the @aws-sdk/lib-storage Upload helper for this path.




