Skip to content
Ravi Agheda
← All writing

— 2 min read

Setup AWS S3 bucket for Node.js

Create a properly scoped S3 bucket and IAM user, and upload files from Node with the AWS SDK v3.

  • AWS
  • Node.js
  • S3

The part of S3 setup that actually matters isn't the SDK call — it's getting the bucket permissions and IAM policy right so you're not left with a publicly-open bucket or an over-privileged access key.

1. Create the bucket

In the S3 console (or via aws s3api create-bucket), create a bucket with Block all public access left on. Files should be reached through signed URLs or your own API, not by making the bucket public.

2. Create a scoped IAM user

Don't reuse root or admin credentials in your app. Create an IAM user with a policy limited to exactly what the app needs — usually just PutObject/GetObject on one bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

Generate an access key for that user and store it as environment variables — never commit it to the repo.

3. Install the SDK

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

4. Upload a file

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

const s3 = new S3Client({ region: "us-east-1" });

async function uploadFile(key, body, contentType) {
  await s3.send(
    new PutObjectCommand({
      Bucket: process.env.S3_BUCKET,
      Key: key,
      Body: body,
      ContentType: contentType,
    })
  );
}

5. Generate a signed URL for downloads

Since the bucket isn't public, give clients a time-limited signed URL instead of a direct object URL:

import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

async function getDownloadUrl(key) {
  const command = new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key: key });
  return getSignedUrl(s3, command, { expiresIn: 3600 });
}

Notes

  • Set a lifecycle rule to expire or transition old objects (to Glacier, or delete) if the bucket is for temporary uploads rather than permanent storage — it's a config toggle, not something you need to build yourself.
  • If uploads come directly from the browser, use a presigned PutObject URL instead of proxying the file through your server.