AWS SDK Best Practices for JavaScript (v3): A Complete Guide to Modern AWS Development
Use modular @aws-sdk/client-* packages, initialize clients outside request handlers for connection reuse, and always consume streaming response bodies to prevent socket exhaustion.
The AWS SDK for JavaScript v3 (aws-sdk-js-v3) is a modular, TypeScript-first client library that replaces the monolithic v2 SDK. These best practices, derived from the agent-toolkit-for-aws repository, cover package structure, credential management, streaming operations, and serverless optimization to help you write efficient, maintainable AWS applications.
Import Only What You Need: Modular Package Structure
The v3 SDK splits every AWS service into its own package under @aws-sdk/client-*. These clients are generated by smithy-typescript and tree-shakeable, meaning bundlers include only the code you actually use.
- Service clients live in
@aws-sdk/client-*(e.g.,@aws-sdk/client-s3). - Higher-level helpers (DynamoDB DocumentClient, S3 multipart upload) are in
@aws-sdk/lib-*. - Utility packages (
@aws-sdk/*) are internal and should not be imported directly.
Always import from the package root. According to skills/core-skills/aws-sdk-js-v3-usage/SKILL.md, deep-path imports break tree-shaking and future compatibility.
// Correct
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
// Incorrect – do not use deep paths
import { S3Client } from "@aws-sdk/client-s3/dist/S3Client";
Choose the Right Client Style: Bare-Bones vs. Aggregated
The SDK offers two import styles. The bare-bones style (preferred) imports only the commands you need, yielding smaller bundle sizes suitable for Lambda and browser apps. The aggregated style imports the entire service namespace, mimicking v2 usage but increasing bundle size significantly.
| Style | Bundle Impact | Use Case |
|---|---|---|
| Bare-bones | Minimal | Lambda, edge functions, SPAs |
| Aggregated | Large | Migration scripts, quick prototypes |
As documented in SKILL.md, prefer the bare-bones style for production serverless workloads.
Configuration and Credential Management
Unlike v2, v3 has no global configuration. Pass a config object to each client constructor, or rely on environment variables like AWS_REGION.
Credential providers from @aws-sdk/credential-providers are lazy-loaded and cached per client until approximately five minutes before expiration. For most Node.js applications, use the default provider chain (env → ini → IMDS/ECS).
import { fromTemporaryCredentials } from "@aws-sdk/credential-providers";
import { S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
region: "us-east-1",
credentials: fromTemporaryCredentials({
params: { RoleArn: "arn:aws:iam::123456789012:role/Reader" }
})
});
Reference skills/core-skills/aws-sdk-js-v3-usage/references/credentials.md for role assumption and named profile patterns.
Connection Reuse and Client Lifecycle
Create clients outside of request handlers (e.g., Lambda handler functions) so they are reused across invocations. This avoids TCP handshake overhead and reduces cold-start latency.
When working with multiple regions, share the underlying HTTP handler and credentials across clients to prevent socket exhaustion. The SKILL.md file demonstrates this pattern under "Share credentials & socket pool across multi-region clients."
// Lambda handler – client initialized outside handler
const s3 = new S3Client({ maxAttempts: 3 });
export const handler = async (event) => {
// Reuse existing client
return await s3.send(new GetObjectCommand({ Bucket: "my-bucket", Key: "data.json" }));
};
Handle Streaming Responses Correctly
S3 GetObject and similar operations return streaming bodies that must be fully consumed or explicitly destroyed. Unread streams keep HTTP sockets open, eventually causing "socket hang up" errors under load.
Use the built-in helper methods transformToString() or transformToByteArray(), or manually call destroy()/cancel() to clean up resources.
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
export async function download(bucket: string, key: string): Promise<string> {
const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
// Body implements the streaming interface
return await Body.transformToString(); // fully consumes the stream
}
Source: skills/core-skills/aws-sdk-js-v3-usage/SKILL.md – Streams section.
DynamoDB DocumentClient for Native Types
Prefer @aws-sdk/lib-dynamodb over the low-level client when working with DynamoDB. The DynamoDBDocumentClient marshals native JavaScript types (strings, numbers, booleans) to and from DynamoDB AttributeValues automatically, removing boilerplate.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
PutCommand,
GetCommand,
} from "@aws-sdk/lib-dynamodb";
const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
await ddb.send(
new PutCommand({
TableName: "Users",
Item: { userId: "123", name: "Alice", age: 30 },
})
);
const { Item } = await ddb.send(new GetCommand({ TableName: "Users", Key: { userId: "123" } }));
console.log(Item?.name); // "Alice"
See skills/core-skills/aws-sdk-js-v3-usage/SKILL.md – DynamoDB DocumentClient section.
S3 Advanced Patterns: Presigned URLs and Multipart Uploads
For browser uploads, generate presigned URLs using @aws-sdk/s3-request-presigner. For large files or streams, use @aws-sdk/lib-storage which handles multipart uploads with automatic retry and progress events.
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
const client = new S3Client({ region: "us-east-1" });
export async function uploadLargeFile(bucket: string, key: string, stream: Readable) {
const upload = new Upload({
client,
params: { Bucket: bucket, Key: key, Body: stream },
queueSize: 4,
partSize: 10 * 1024 * 1024, // 10 MiB parts
});
upload.on("httpUploadProgress", p => console.log("Progress:", p));
await upload.done();
}
Also utilize waiters like waitUntilObjectExists for built-in polling logic. Full details are in skills/core-skills/aws-sdk-js-v3-usage/references/s3.md.
Automatic Pagination with Paginators
Replace manual NextToken handling with generated paginate* helpers. They return async iterables that yield each page, handling continuation tokens automatically.
import { DynamoDBClient, paginateListTables } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({});
const tableNames: string[] = [];
for await (const page of paginateListTables({ client }, {})) {
tableNames.push(...page.TableNames ?? []);
}
console.log(tableNames);
Error Handling Strategies
All service-specific errors inherit from *ServiceException (e.g., S3ServiceException). Inspect $metadata for HTTP status codes, request IDs, and extended information. Use instanceof checks or e.name for fine-grained control.
try {
await client.send(new GetObjectCommand({ Bucket: "my-bucket", Key: "missing" }));
} catch (error) {
if (error.name === "NoSuchKey") {
console.log("Object not found");
}
console.log(error.$metadata.httpStatusCode); // 404
}
Reference skills/core-skills/aws-sdk-js-v3-usage/references/error-handling.md for the complete error hierarchy.
Middleware for Cross-Cutting Concerns
Add custom logic (logging, tracing, request transformation) to the client's middleware stack. Middleware runs in this order: initialize → serialize → build → finalizeRequest → deserialize.
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const client = new S3Client({});
client.middlewareStack.add(
(next, context) => async (args) => {
console.log("Calling:", context.commandName, args.input);
const result = await next(args);
console.log("Result:", result.output);
return result;
},
{ name: "logMiddleware", step: "initialize" }
);
Cancellable Requests with AbortController
Wrap long-running operations with an AbortController to support request cancellation. Pass the signal via the abortSignal option in the command call.
import { AbortController } from "@aws-sdk/abort-controller";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const abort = new AbortController();
const client = new S3Client({});
const promise = client.send(
new PutObjectCommand({ Bucket: "my-bucket", Key: "large-file", Body: stream }),
{ abortSignal: abort.signal }
);
// Cancel after 5 seconds if still running
setTimeout(() => abort.abort(), 5_000);
await promise; // throws AbortError if aborted
Lambda and Serverless Optimization
For AWS Lambda functions, initialize clients outside the handler to enable connection reuse across warm invocations. Perform heavy async setup lazily inside the first invocation if necessary, avoiding cold-start penalties.
const client = new S3Client({}); // Outside handler
export const handler = async (event) => {
// Client reused from previous invocation if warm
return client.send(new GetObjectCommand({ Bucket: event.bucket, Key: event.key }));
};
Source: skills/core-skills/aws-sdk-js-v3-usage/SKILL.md – Lambda Best Practices.
TypeScript Enhancements
Remove unwanted | undefined from response types using AssertiveClient from @smithy/types. Narrow streaming blob types using NodeJsClient or BrowserClient providers depending on your target runtime.
See skills/core-skills/aws-sdk-js-v3-usage/references/typescript.md for advanced type narrowing techniques.
Multi-Region Access Points and SigV4a
For features requiring the SigV4a signing algorithm (e.g., S3 Multi-Region Access Points), install one side-effect package: @aws-sdk/signature-v4-crt (Node.js only) or @aws-sdk/signature-v4a (Node.js + browser). Only one is required; do not install both.
Reference: skills/core-skills/aws-sdk-js-v3-usage/references/sigv4a.md.
Summary
- Import from package roots (
@aws-sdk/client-s3) rather than deep paths to ensure tree-shaking compatibility. - Initialize clients outside handlers in Lambda to reuse connections and reduce latency.
- Consume streaming bodies using
transformToString()ordestroy()to prevent socket exhaustion. - Use paginators (
paginateListTables) instead of manual token handling for cleaner async iteration. - Prefer DocumentClient (
@aws-sdk/lib-dynamodb) for automatic type marshaling. - Handle errors by checking
error.nameor usinginstanceofagainst*ServiceExceptionclasses.
Frequently Asked Questions
What is the main difference between AWS SDK for JavaScript v2 and v3?
v3 is a modular, TypeScript-first rewrite where each service lives in its own package (@aws-sdk/client-s3 vs. the monolithic aws-sdk). This reduces bundle sizes by up to 75% and provides first-class TypeScript support with generated type definitions, whereas v2 required additional type packages.
How do I upload large files to S3 using the JavaScript v3 SDK?
Use @aws-sdk/lib-storage which provides an Upload class that automatically handles multipart uploads, parallelizes parts using queueSize, and emits httpUploadProgress events. Set partSize to 5MB or higher, and pass your S3Client instance to the Upload constructor.
Why do I get "socket hang up" errors in Lambda when using the AWS SDK?
This typically occurs when streaming response bodies from S3 GetObject or similar operations are not consumed. Always call .transformToString(), .transformToByteArray(), or .destroy() on the Body stream to release the HTTP socket back to the pool.
How do I share AWS clients across multiple regions efficiently?
Instantiate separate clients for each region but share the same credentials provider and HTTP handler instance. As documented in skills/core-skills/aws-sdk-js-v3-usage/SKILL.md, this prevents credential resolution overhead and socket pool fragmentation when operating across regions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →