Port a Python boto3 Script to Node.js With AWS SDK v3

Open laptop on a wooden desk showing a code editor with colorful source code

Photo by Justin Morgan on Unsplash

To port a Python AWS script to Node.js SDK v3, replace each boto3.client() with a modular client such as EC2Client, call operations with client.send(new XCommand(input)), and await every call. Swap get_paginator for paginateX, get_waiter for waitUntilX, and ClientError codes for err.name. Parameter names stay the same, because both SDKs use the AWS API’s names.

This guide is for developers who have a working boto3 script and need it in Node.js, usually to run it in a TypeScript codebase or a Node.js Lambda. Moving a Python AWS script to Node.js SDK v3 is mostly mechanical, and our converter handles that part. What’s left are the decisions a converter can’t make for you: what replaces boto3 resources, how waiting and retrying differ, where credentials and the region come from, and how to package the result. If the Node.js codebase you’re porting into still uses aws-sdk v2 itself, follow the plan to migrate a Node.js app from AWS SDK v2 to v3 first.

You’ll get a concept-by-concept map, a real boto3 script with its complete TypeScript port, the IAM policy it needs, and the errors you’re likely to hit on the first run.

What changes when you move from boto3 to AWS SDK for JavaScript v3

Concept boto3 (Python) AWS SDK v3 (Node.js)
Client boto3.client("ec2") new EC2Client({}) from @aws-sdk/client-ec2
Calling an operation ec2.stop_instances(InstanceIds=ids) await ec2.send(new StopInstancesCommand({ InstanceIds: ids }))
Parameters PascalCase API names The same PascalCase names
Resources (boto3.resource) s3.Bucket("b").objects.all() No equivalent; use the client and paginators
Pagination get_paginator("describe_instances") paginateDescribeInstances({ client }, input)
Waiters get_waiter("instance_stopped").wait() waitUntilInstanceStopped({ client, maxWaitTime }, input)
Errors ClientError, e.response["Error"]["Code"] Service exception class, err.name
Timestamps datetime Date
Streaming bodies obj["Body"].read() await Body.transformToString()
Region env var AWS_DEFAULT_REGION AWS_REGION
Dependencies requirements.txt package.json, one package per service

The parameter row is the good news. InstanceIds, Filters and MaxResults are spelled the same in both SDKs, so your request dictionaries become object literals almost unchanged. The response fields match too, but in TypeScript they’re typed as optional, so you’ll write page.Reservations ?? [] where Python would just index.

Prerequisites

  • Node.js 20 or later. The AWS SDK for JavaScript v3 repository’s Node.js support policy says v3.968.0 and higher require Node.js 20.
  • The client package for each service the script uses, such as @aws-sdk/client-ec2 or @aws-sdk/client-s3.
  • tsx to run TypeScript directly, or plain JavaScript if you prefer.
  • The same AWS profile or role the Python script used.

How to port a Python AWS script to Node.js, step by step

  1. List the callsWrite down every boto3 operation, paginator and waiter the script uses. That list is your package list and your IAM policy.
  2. Draft the translationPaste the script into the free boto3 to AWS SDK JS v3 converter to get the command-by-command rewrite. Strip hard-coded keys first.
  3. Replace resource-style codeRewrite any boto3.resource usage as client calls with paginators.
  4. Make the control flow asyncPut the logic in an async function and await every send, paginator loop and waiter.
  5. Port error handlingMap each ClientError code check to an err.name check.
  6. Check credentials and regionRun with AWS_PROFILE and AWS_REGION set explicitly the first time.
  7. Compare outputRun both scripts read-only against the same account and diff what they print.

Example: a boto3 script and its Node.js SDK v3 port

This boto3 script stops every running EC2 instance tagged Environment=dev and waits until they’re stopped. It uses a paginator, a waiter and a ClientError check, which covers most of what real scripts do.

stop_dev_instances.py (before)

import sys

import boto3
from botocore.exceptions import ClientError

ec2 = boto3.client("ec2")


def running_dev_instances():
    paginator = ec2.get_paginator("describe_instances")
    pages = paginator.paginate(
        Filters=[
            {"Name": "tag:Environment", "Values": ["dev"]},
            {"Name": "instance-state-name", "Values": ["running"]},
        ]
    )
    ids = []
    for page in pages:
        for reservation in page["Reservations"]:
            for instance in reservation["Instances"]:
                ids.append(instance["InstanceId"])
    return ids


def main():
    ids = running_dev_instances()
    if not ids:
        print("No running dev instances.")
        return
    print(f"Stopping {len(ids)} instance(s): {', '.join(ids)}")
    try:
        ec2.stop_instances(InstanceIds=ids)
    except ClientError as err:
        if err.response["Error"]["Code"] == "UnauthorizedOperation":
            sys.exit("Missing ec2:StopInstances permission.")
        raise
    # Default waiter config: poll every 15 seconds, up to 40 attempts
    ec2.get_waiter("instance_stopped").wait(InstanceIds=ids)
    print("All stopped.")


if __name__ == "__main__":
    main()

The Node.js port with AWS SDK v3, in TypeScript:

stop-dev-instances.ts (after)

// stop-dev-instances.ts
// Stops every running EC2 instance tagged Environment=dev and waits until they are stopped.
import {
  EC2Client,
  EC2ServiceException,
  StopInstancesCommand,
  paginateDescribeInstances,
  waitUntilInstanceStopped,
} from "@aws-sdk/client-ec2";

const ec2 = new EC2Client({}); // region from AWS_REGION or the profile in ~/.aws/config

async function runningDevInstances(): Promise<string[]> {
  const ids: string[] = [];
  const pages = paginateDescribeInstances(
    { client: ec2 },
    {
      Filters: [
        { Name: "tag:Environment", Values: ["dev"] },
        { Name: "instance-state-name", Values: ["running"] },
      ],
    },
  );
  for await (const page of pages) {
    for (const reservation of page.Reservations ?? []) {
      for (const instance of reservation.Instances ?? []) {
        if (instance.InstanceId) {
          ids.push(instance.InstanceId);
        }
      }
    }
  }
  return ids;
}

async function main(): Promise<void> {
  const ids = await runningDevInstances();
  if (ids.length === 0) {
    console.log("No running dev instances.");
    return;
  }
  console.log(`Stopping ${ids.length} instance(s): ${ids.join(", ")}`);

  try {
    await ec2.send(new StopInstancesCommand({ InstanceIds: ids }));
  } catch (err) {
    if (err instanceof EC2ServiceException && err.name === "UnauthorizedOperation") {
      console.error("Missing ec2:StopInstances permission.");
      process.exit(1);
    }
    throw err;
  }

  // boto3's default (15 s x 40 attempts) is about 600 seconds in total.
  await waitUntilInstanceStopped({ client: ec2, maxWaitTime: 600 }, { InstanceIds: ids });
  console.log("All stopped.");
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
Terminal

npm init -y
npm install @aws-sdk/client-ec2
npm install --save-dev tsx typescript @types/node

AWS_PROFILE=dev-admin AWS_REGION=us-east-1 npx tsx stop-dev-instances.ts

The TypeScript version type-checks cleanly against the current @aws-sdk/client-ec2. If you’d rather not port by hand at all, the example that shows how to detect and stop underutilized EC2 instances by CPU is already written for SDK v3.

Resources, paginators, waiters and exceptions in detail

boto3 resources have no v3 equivalent

Code like for obj in s3.Bucket("logs").objects.filter(Prefix="2026/") hides pagination, lazy loading and batching. v3 has only clients, so you write the loop yourself with paginateListObjectsV2. That’s less magic but more explicit about how many requests you send. The boto3 team has also said it doesn’t plan new features for the resources interface, so client-style code is the better starting point in either language.

Paginators: same idea, async iteration

A boto3 paginator is a synchronous iterator; a v3 paginator is an async generator, consumed with for await. Pass pageSize in the first argument if you need to control page size. Not every operation has a v3 paginator; when one is missing, loop on the continuation token (NextToken, NextKeyMarker and so on) yourself.

Waiters: attempts versus seconds

boto3 waiters count attempts with a fixed delay (WaiterConfig={"Delay": 15, "MaxAttempts": 40}). v3 waiters take a required maxWaitTime in seconds and back off between minDelay and maxDelay; the EC2 waiters default to 15 and 120 seconds. Multiply delay by attempts to get an equivalent maxWaitTime. A v3 waiter throws when it times out or reaches a failure state, so wrap it in try if the script should continue.

Exceptions: codes become names

In boto3 you catch ClientError and read err.response["Error"]["Code"]. In v3 every service error extends the service’s base exception (EC2ServiceException, S3ServiceException), and the code is in err.name. Many services also export modeled classes you can test with instanceof, such as NoSuchKey from @aws-sdk/client-s3. The HTTP status sits in err.$metadata.httpStatusCode.

Credentials, regions and packaging

Credentials work the same way. Both SDKs use a default chain that reads environment variables, AWS_PROFILE, SSO and shared files, and role credentials on AWS compute. Where Python used boto3.Session(profile_name="ops"), use fromIni({ profile: "ops" }) from @aws-sdk/credential-providers, or just set AWS_PROFILE. The guide to AWS SDK v3 credential providers: fromIni, fromSSO and assume role covers the other options.

Region is the classic surprise. boto3 reads AWS_DEFAULT_REGION; the v3 developer guide lists AWS_REGION, then the profile’s region in ~/.aws/config. A CI job that only sets AWS_DEFAULT_REGION fails in Node.js with a missing-region error. Set AWS_REGION, or pass region to the client.

Packaging moves from one boto3 install to one package per service, which keeps Lambda bundles small. Pin all @aws-sdk/client-* packages to the same version, since mismatched versions are the SDK team’s most common cause of TypeScript type errors. If you write plain JavaScript with top-level await, set "type": "module" in package.json, as described in the Node.js documentation for the package.json type field.

Retries carry over too: botocore’s Config(retries={"max_attempts": 10}) becomes new EC2Client({ maxAttempts: 10 }). Connect and read timeouts move to the request handler; the guide to configure retries and timeouts in AWS SDK v3 covers each setting.

IAM permissions the ported script needs

The port calls exactly the same API operations, so the IAM policy doesn’t change. For the example, this least-privilege policy allows the describe call everywhere and the stop call only on instances tagged Environment=dev. DescribeInstances doesn’t support resource-level restrictions, so it needs "Resource": "*".

stop-dev-instances-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "FindInstances",
      "Effect": "Allow",
      "Action": "ec2:DescribeInstances",
      "Resource": "*"
    },
    {
      "Sid": "StopDevInstances",
      "Effect": "Allow",
      "Action": "ec2:StopInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringEquals": { "aws:ResourceTag/Environment": "dev" }
      }
    }
  ]
}

For your own scripts, the IAM policy generator for Python code drafts a policy from the boto3 version, and the IAM policy generator for TypeScript code from the port. Both should produce the same actions; if they don’t, one of the scripts calls something the other doesn’t. To check the port by hand, map each SDK v3 Command to its IAM action.

Troubleshooting the first run

  • Region is missing. Only AWS_DEFAULT_REGION is set. Set AWS_REGION or add region to the profile.
  • Could not load credentials from any providers. The profile name is wrong or the SSO session expired. Run aws sso login and check AWS_PROFILE.
  • UnauthorizedOperation or AccessDenied. The Node.js run uses a different identity than the Python one did. Our guide to troubleshoot AWS IAM access denied errors step by step shows how to find the missing action, and the script to check the permissions of your current IAM role confirms who you are.
  • The script exits before work finishes. A call isn’t awaited, or a forEach with an async callback is used. Use for...of with await, or Promise.all.
  • Cannot read properties of undefined. Python raised KeyError on a missing field; TypeScript returns undefined. Use ?? [] and optional chaining on response fields.
  • Different results between the two scripts. Usually a region or filter difference. Print the resolved region with await ec2.config.region().

What the converter can’t decide for you

The boto3 to v3 converter rewrites calls, not intent. It can’t know whether your resource-style loop should become a paginator or a single call, how long a waiter should run, which errors are expected, or where the code will run. It flags spots with no direct equivalent in comments, and the output must be reviewed and tested. See the free converter limits of 6 per minute and 60 per day and read what happens to code you paste into an AI converter first. The reverse trip uses the AWS SDK JS to boto3 converter.

For more ready-made v3 code, browse the AWS SDK v3 practical examples in TypeScript, such as how to invoke a Lambda function with AWS SDK v3. And if the script only answered a question, you may not need it at all: ChatWithCloud writes and runs the SDK code for questions like “which dev instances are running?” with your AWS profile, as shown in the guide to list AWS resources in plain English from your terminal.

Frequently asked questions

Is there a boto3 equivalent for Node.js?

The AWS SDK for JavaScript v3. It has one package per service, such as @aws-sdk/client-s3, and covers the same APIs as boto3’s clients. It has no equivalent of boto3’s resource interface.

Do boto3 parameter names change in SDK v3?

No. Both use the AWS API’s PascalCase member names, such as InstanceIds and Filters. Method names change from stop_instances to StopInstancesCommand.

How do I catch a specific AWS error in Node.js SDK v3?

Check err.name against the error code, or use instanceof with a modeled exception class exported by the client package. The base class, such as S3ServiceException, gives typed access to $metadata.

Should I port to JavaScript or TypeScript?

TypeScript catches the missing-field bugs that Python surfaced as KeyError, and the SDK ships full types. Plain JavaScript works with the same code minus the annotations.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud