What you'll learn

By the end of this you'll know how to set up AWS for a Node.js project from scratch: create an IAM user with minimal permissions, install and configure the AWS CLI and SDK v3, and make your first real AWS API call from Node.js code. You'll understand the credential chain, what regions are and why they matter, and the SDK pattern that works identically across every AWS service.

This is the setup that every AWS-based Node project needs regardless of which services it eventually uses.

Who this is for

  • Node.js developers who know they need AWS but haven't connected the two yet
  • Developers who've copied AWS SDK code before without fully understanding how credentials flow
  • Anyone starting a project that involves S3, Lambda, SQS, or any other AWS service and wants to get the foundation right before going further

You can skip this if you're already configuring IAM policies, the AWS CLI, and SDK clients without looking things up. Come back if you're onboarding a teammate who needs the fundamentals, or if you're chasing a credentials error you can't quite place.

What is AWS in a Node.js context?

AWS (Amazon Web Services) is a collection of cloud services — storage, compute, queues, databases, email, and hundreds more — that your Node.js code can call over HTTPS using the AWS SDK. Each service has its own API; the SDK wraps those APIs into TypeScript-friendly clients that you import and call as ordinary async functions.

Plain English: AWS is a set of services you pay for by use. Your Node.js code talks to them the same way it talks to any external API — over HTTPS — except the AWS SDK handles authentication, request signing, and retries automatically.

Simple idea: pick a service, install its client package, create a client with your credentials, and call methods. That's the entire pattern for every AWS service.

Prerequisites

  • Node.js 18 or higher (node -v to check)
  • An AWS account (free tier is sufficient to follow this tutorial)
  • npm and a terminal

Setup from zero

Step 1 — Create an IAM user

Never use your root AWS account credentials in application code. The root account has unrestricted access and no safety net. Create a dedicated IAM user instead:

  1. Sign into the AWS Console → IAMUsersCreate user
  2. Name it something meaningful: my-app-dev
  3. Choose Programmatic access (access key ID and secret, not a Console login)
  4. Attach a scoped permission policy — start specific, for example AmazonS3ReadOnlyAccess or a custom policy for exactly the actions your app needs
  5. On the final screen, copy or download the Access Key ID and Secret Access Key — you won't see the secret again after leaving this page

> Little tip: Never paste access keys into source files or commit them to git — not even to a private repository, not even briefly. Private repos get exposed, GitHub scans for leaked keys, and AWS itself watches for them. Use environment variables or the credentials file for local development, and IAM roles (no keys at all) for code running on AWS infrastructure.

Step 2 — Configure the AWS CLI

Install the AWS CLI for your platform following the instructions at the official AWS documentation, then configure a named profile:

aws configure --profile my-app-dev

You'll be prompted for your Access Key ID, Secret Access Key, default region (us-east-1 if unsure), and output format (json). The CLI writes these to ~/.aws/credentials and ~/.aws/config.

Test that the credentials work:

aws sts get-caller-identity --profile my-app-dev

A successful response returns your account ID, user ID, and ARN. If it does, the credentials are valid and the CLI is ready.

Step 3 — Install the SDK and make your first call

The AWS SDK v3 is modular — you install only the client packages you actually use:

npm install @aws-sdk/client-s3

Add your credentials to .env:

AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=your-secret-here
AWS_REGION=us-east-1

Create a client and list S3 buckets:

import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({ region: process.env.AWS_REGION });

const response = await s3.send(new ListBucketsCommand({}));
console.log(response.Buckets);

The SDK automatically reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from environment variables — no explicit credential passing needed in the constructor.

The mental model

Think of AWS as a collection of regional services accessed through a unified credential and signing system.

Every AWS resource lives in a region — a physical data center location like us-east-1 (Northern Virginia) or eu-west-1 (Ireland). Regions are isolated from each other by default. An S3 bucket created in us-east-1 is not visible to a client configured for eu-west-1. When you create a SDK client, you tell it which region to talk to.

Credentials tell AWS who is making the request. The SDK resolves them through a credential chain, checked in this order: explicit values in the client constructor, environment variables (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY), the ~/.aws/credentials file, and finally — when running on EC2, Lambda, ECS, or EKS — the attached IAM role. This chain means your code doesn't change between local development and production on AWS infrastructure.

Services are independent APIs. Each has its own client package in SDK v3: @aws-sdk/client-s3, @aws-sdk/client-lambda, @aws-sdk/client-ses, @aws-sdk/client-sqs. Every client follows the same pattern: create it, create a Command object, send the Command, await the response.

Key terms

ARN (Amazon Resource Name) — a globally unique identifier for every AWS resource. Format: arn:aws:s3:::my-bucket. Used in IAM policies to specify exactly which resource a permission applies to.

Region — a geographic cluster of data centers. Resources are region-specific unless explicitly global (like IAM). Always set the region in your client or SDK configuration.

IAM (Identity and Access Management) — the AWS service that controls who can do what. Users, roles, groups, and policies live here. A policy document in JSON defines which actions on which resources are allowed or denied.

SDK v3 — the current AWS SDK for JavaScript and TypeScript. Modular, tree-shakeable, and native ESM-compatible. SDK v2 is in maintenance mode; use v3 for all new work.

Credential chain — the ordered sequence the SDK uses to find credentials: explicit config, environment variables, credentials file, EC2/Lambda instance role. The same code works locally and on AWS without modification.

Command — in SDK v3, an object representing one API operation. You instantiate it with input parameters and pass it to client.send(). Every operation in every service follows this pattern.

Step-by-step: uploading a file from Node.js

npm install @aws-sdk/client-s3
// src/lib/s3-upload.ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFileSync } from "fs";

const s3 = new S3Client({ region: process.env.AWS_REGION! });

export async function uploadFile(
  bucket: string,
  key: string,
  filePath: string,
  contentType: string
): Promise<void> {
  const body = readFileSync(filePath);

  await s3.send(
    new PutObjectCommand({
      Bucket: bucket,
      Key: key,
      Body: body,
      ContentType: contentType,
    })
  );

  console.log(`Uploaded → s3://${bucket}/${key}`);
}

Call it:

await uploadFile("my-bucket", "images/photo.jpg", "./photo.jpg", "image/jpeg");

The Key is the object's path within the bucket. Think of it as a file path: images/photo.jpg means a folder called images (a prefix, not a real folder) and a file called photo.jpg.

> Little tip: SDK v3 commands are plain objects — you can log them before sending to inspect the exact input being constructed. console.log(new PutObjectCommand({ ... })) prints all parameters before touching AWS, which is useful when debugging a missing or wrong parameter.

Patterns

One client per service, created at module level — create the S3Client once when the module loads and reuse it across calls. SDK clients maintain connection pools. Recreating them on every request adds latency and wastes resources.

Handle SDK errors by name — SDK v3 throws typed errors with a name property matching the AWS error code:

try {
  await s3.send(new GetObjectCommand({ Bucket: "bucket", Key: "missing.txt" }));
} catch (err: unknown) {
  if (err instanceof Error && err.name === "NoSuchKey") {
    return null; // expected — the object doesn't exist
  }
  throw err; // unexpected — let it propagate
}

Rotate keys via environment variables — when an access key needs rotation, update the environment variable in your platform (Render, Fly.io, ECS). The next process start picks up the new value. No code changes.

Common mistakes

Using root account credentials — the root account has unrestricted access, including billing and account deletion. A compromised root key is an incident with no upper bound. Create IAM users or roles for all application code.

Hardcoding credentials in source — bots scan public and private repositories for access key patterns within seconds of a push. Treat a committed key as immediately compromised, rotate it, and use environment variables going forward.

Creating clients inside hot paths — a new SDK client created on every incoming request recreates connection pools on every call. Module-level initialization is the correct pattern.

Troubleshooting

UnrecognizedClientException — the access key ID is invalid, inactive, or doesn't match the region the client is pointed at. Check that AWS_ACCESS_KEY_ID matches the key shown in IAM → your user → Security credentials, and that its status is Active.

AccessDeniedException — the IAM user or role lacks the required permission. Open IAM → your user → Permissions and inspect the attached policies. The error message includes the action that was denied (e.g., s3:PutObject). Add that action to the policy.

Region mismatch — an S3 client configured for us-east-1 talking to a bucket created in eu-west-1 returns a redirect or permanent redirect error. Set the client region to match the bucket region.

Checklist

  • [ ] IAM user created with programmatic access and a scoped permission policy
  • [ ] Access keys stored in .env or platform environment variables only
  • [ ] .env is in .gitignore
  • [ ] AWS CLI configured with aws configure and verified with sts get-caller-identity
  • [ ] SDK v3 client packages installed (@aws-sdk/client-*)
  • [ ] SDK clients created once at module level, not inside request handlers
  • [ ] Region set explicitly in each client constructor via process.env.AWS_REGION
  • [ ] Errors caught and checked by err.name for expected failure modes

Practice task

Set up a fresh Node.js TypeScript project with SDK v3. Create an IAM user with AmazonS3ReadOnlyAccess only. Configure its credentials as environment variables. Write a script that lists all S3 buckets, picks the first one, and lists up to ten objects inside it — printing each object's Key and size. Run it and confirm the output. Then remove the permission from the IAM policy and run again to observe the AccessDeniedException and read the error details.

FAQ

Should I use SDK v2 or v3?

SDK v2 is in maintenance-only mode. Use v3 for all new projects. v3 is modular — you only ship the code for the services you use, which matters for Lambda cold starts and edge runtimes where bundle size is a real performance factor.

What's the difference between an IAM user and an IAM role?

A user has long-lived access keys tied to a person or application. A role has temporary credentials issued automatically to AWS services or users who assume it. Prefer roles for code running on AWS — no keys to manage, rotate, or accidentally leak. Use users for local development when roles aren't available.

Do I need a separate IAM user per environment?

At minimum, production should use different credentials from development. Sharing a key between dev and prod means a leaked dev key has production access. In practice, production on AWS infrastructure should use an IAM role with no keys at all.

What to learn next

With the SDK setup solid: Amazon S3 for file storage patterns including presigned URLs and bucket policies, AWS Lambda for running Node.js without managing servers, and IAM least privilege for designing permission policies that match what your application actually does.

  • Amazon S3 for developers — file storage patterns built on the SDK foundation covered here
  • AWS Lambda basics — serverless Node.js functions using the same SDK pattern
  • IAM least privilege — designing permission policies that grant exactly what the application needs

Takeaways

AWS for Node.js follows one pattern for every service: IAM credentials authorize the request, the SDK credential chain finds those credentials automatically, and each service has its own client package with a consistent client.send(Command) interface. Getting the credential setup right once means every additional AWS service you add later just needs a new npm install.

If you remember only one thing: never put AWS credentials in source code. Use environment variables locally and IAM roles on AWS infrastructure. A key in a git commit — even a private repo, even briefly — should be treated as compromised and rotated immediately.