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-ec2or@aws-sdk/client-s3. tsxto 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
- List the callsWrite down every boto3 operation, paginator and waiter the script uses. That list is your package list and your IAM policy.
- 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.
- Replace resource-style codeRewrite any
boto3.resourceusage as client calls with paginators. - Make the control flow asyncPut the logic in an
asyncfunction andawaiteverysend, paginator loop and waiter. - Port error handlingMap each
ClientErrorcode check to anerr.namecheck. - Check credentials and regionRun with
AWS_PROFILEandAWS_REGIONset explicitly the first time. - 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.
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
// 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);
});
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": "*".
{
"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. OnlyAWS_DEFAULT_REGIONis set. SetAWS_REGIONor addregionto the profile.Could not load credentials from any providers. The profile name is wrong or the SSO session expired. Runaws sso loginand checkAWS_PROFILE.UnauthorizedOperationorAccessDenied. 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
forEachwith an async callback is used. Usefor...ofwithawait, orPromise.all. Cannot read properties of undefined. Python raisedKeyErroron a missing field; TypeScript returnsundefined. 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
