infra-platform-aws-sdk
AWS SDK v3 for TypeScript — modular clients, command pattern, S3, DynamoDB, SQS, Lambda, SNS, Secrets Manager
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-platform-aws-sdk/skills/infra-platform-aws-sdk
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
AWS SDK v3 Patterns
Quick Guide: AWS SDK v3 for JavaScript/TypeScript uses modular packages (
@aws-sdk/client-*) with a command pattern: create a client, instantiate a command, callclient.send(command). Import only the services you need for tree-shaking. UseDynamoDBDocumentClientfor native JS types. UsegetSignedUrlfrom@aws-sdk/s3-request-presignerfor presigned URLs. Handle errors withinstanceofspecific exception classes. Use built-in paginators (paginate*) withfor await...of.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use AWS SDK v3 modular packages (@aws-sdk/client-*) — NEVER the monolithic aws-sdk v2 package)
(You MUST use the command pattern: client.send(new XxxCommand({...})) — NEVER call methods directly on the client)
(You MUST use DynamoDBDocumentClient from @aws-sdk/lib-dynamodb for DynamoDB — it auto-marshalls native JS types)
(You MUST handle errors with instanceof specific exception classes — NEVER catch generic Error and check .code)
(You MUST use built-in paginators (paginate* functions) for paginated APIs — NEVER manually track continuation tokens)
</critical_requirements>
Examples
- Core Patterns — Client setup, S3 operations, DynamoDB basics, credential providers, error handling, pagination
- Messaging — SQS send/receive/delete, SNS publish, FIFO queues, dead-letter patterns
- Advanced — Lambda invocation, Secrets Manager, presigned URLs, middleware, streaming
- Quick Reference — Package cheat sheet, import patterns, error handling decision tree, credential provider chain
Auto-detection: AWS SDK, @aws-sdk/client, S3Client, DynamoDBClient, DynamoDBDocumentClient, SQSClient, LambdaClient, SNSClient, SecretsManagerClient, PutObjectCommand, GetObjectCommand, GetCommand, PutCommand, QueryCommand, SendMessageCommand, InvokeCommand, GetSecretValueCommand, getSignedUrl, s3-request-presigner, credential-providers, fromEnv, fromIni, paginateListObjectsV2, aws-sdk-client-mock
When to use:
- Interacting with any AWS service from TypeScript/JavaScript
- S3 file operations (upload, download, presigned URLs, listings)
- DynamoDB CRUD operations and queries
- SQS message sending, receiving, and queue management
- Lambda function invocation from other services
- SNS topic publishing and notifications
- Secrets Manager secret retrieval
- Custom middleware for request/response modification
When NOT to use:
- Infrastructure provisioning (use an IaC tool)
- AWS console-only operations with no SDK equivalent
- Simple CLI-only tasks better served by the AWS CLI directly
Key patterns covered:
- Modular client setup with typed configuration
- Command pattern (
client.send(new Command({...}))) - S3: upload, download, delete, list, presigned URLs, streaming
- DynamoDB:
DynamoDBDocumentClientwithGet,Put,Query,Update,Delete - SQS: send, receive, delete messages, long polling, FIFO
- SNS: publish to topics, message attributes
- Lambda: synchronous and asynchronous invocation
- Secrets Manager: secret retrieval with caching
- Credential provider chain and explicit providers
- Error handling with
instanceofexception classes and$metadata - Pagination with async iterators
- Middleware stack customization
- Retry configuration
<decision_framework>
Decision Framework
Choosing the Right DynamoDB Client
Are you working with DynamoDB?
|
+-- Need native JS objects (recommended) --> DynamoDBDocumentClient from @aws-sdk/lib-dynamodb
| +-- Import Get/Put/Query/Update/Delete Commands from @aws-sdk/lib-dynamodb
|
+-- Need raw AttributeValue format --> DynamoDBClient from @aws-sdk/client-dynamodb
+-- Import commands from @aws-sdk/client-dynamodb
+-- Manually marshall/unmarshall with @aws-sdk/util-dynamodb
Choosing Between Synchronous and Async Lambda Invocation
Do you need the Lambda response immediately?
|
+-- YES --> InvocationType: "RequestResponse" (synchronous, waits for result)
|
+-- NO --> InvocationType: "Event" (async, returns immediately, 3 retries)
Credential Provider Selection
Where is this code running?
|
+-- Lambda / ECS / EC2 --> Default chain (auto-detects IAM role) — no config needed
|
+-- Local development --> fromIni() (reads ~/.aws/credentials) or fromEnv()
|
+-- CI/CD pipeline --> fromEnv() with AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
|
+-- Cross-account access --> fromTemporaryCredentials() with STS AssumeRole
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Using the monolithic
aws-sdkv2 package — it ships the entire SDK (~70 MB). Use modular@aws-sdk/client-*packages. - Calling methods directly on the client (v2 style:
s3.getObject()) — use the command pattern:s3.send(new GetObjectCommand({...})). - Using raw
DynamoDBClientcommands with manualmarshall()/unmarshall()— useDynamoDBDocumentClientfrom@aws-sdk/lib-dynamodb. - Catching errors with
.codestring comparison (v2 style) — useinstanceofwith typed exception classes. - Manually tracking pagination tokens in a while loop — use built-in
paginate*functions withfor await...of. - Hardcoding AWS credentials in source code — use the credential provider chain or environment variables.
Medium Priority Issues:
- Creating a new client instance per request — create clients once and reuse them (they manage connection pooling).
- Not setting a region explicitly — defaults vary by environment and cause confusing errors.
- Mixing
@aws-sdk/client-dynamodband@aws-sdk/lib-dynamodbcommand imports — pick one approach per codebase. - Missing
ContentTypeon S3PutObject— S3 defaults toapplication/octet-stream, breaking browser downloads. - Not buffering the S3
GetObjectresponse body —response.Bodyis a stream; call.transformToString()or.transformToByteArray().
Gotchas and Edge Cases:
GetObjectresponse body is aReadableStream(not a string) — you must consume it with.transformToString(),.transformToByteArray(), or pipe it to a writable stream.DynamoDBDocumentClientcommands come from@aws-sdk/lib-dynamodb, NOT@aws-sdk/client-dynamodb— importing from the wrong package gives you rawAttributeValuetypes.- Presigned URLs require the separate
@aws-sdk/s3-request-presignerpackage — it is NOT included in@aws-sdk/client-s3. InvokeCommandreturnsPayloadas aUint8Array— decode withnew TextDecoder().decode(response.Payload)beforeJSON.parse.- SDK v3 version mismatches across client packages cause TypeScript errors — pin all
@aws-sdk/*packages to the same version range. - In Lambda, the SDK is bundled in the runtime but may be outdated — bundle your own version for latest features.
SQS ReceiveMessageCommandmay returnMessages: undefined(not empty array) when no messages are available — always useresponse.Messages ?? [].- Presigned URL expiry is capped at 7 days, but temporary credentials may expire sooner — the URL stops working when the signing credentials expire.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use AWS SDK v3 modular packages (@aws-sdk/client-*) — NEVER the monolithic aws-sdk v2 package)
(You MUST use the command pattern: client.send(new XxxCommand({...})) — NEVER call methods directly on the client)
(You MUST use DynamoDBDocumentClient from @aws-sdk/lib-dynamodb for DynamoDB — it auto-marshalls native JS types)
(You MUST handle errors with instanceof specific exception classes — NEVER catch generic Error and check .code)
(You MUST use built-in paginators (paginate* functions) for paginated APIs — NEVER manually track continuation tokens)
Failure to follow these rules will cause bloated bundles (v2), lost type safety (direct calls), marshalling bugs (raw DynamoDB), and fragile error handling (string comparison).
</critical_reminders>
Files (skills)
-
examples
-
advanced.md 8.8 KB
# AWS SDK v3 — Advanced Patterns > Lambda invocation, Secrets Manager, presigned URLs, middleware, and streaming. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Core Patterns](core.md) — Client setup, S3, DynamoDB, error handling - [Messaging](messaging.md) — SQS, SNS patterns --- ## Lambda — Synchronous Invocation ```typescript import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; const lambda = new LambdaClient({}); interface ProcessResult { status: string; processedAt: string; } export async function invokeProcessor(orderId: string): Promise<ProcessResult> { const response = await lambda.send( new InvokeCommand({ FunctionName: "order-processor", InvocationType: "RequestResponse", // Synchronous — waits for result Payload: JSON.stringify({ orderId }), }), ); if (response.FunctionError) { const errorPayload = JSON.parse(new TextDecoder().decode(response.Payload)); throw new Error(`Lambda error: ${errorPayload.errorMessage}`); } return JSON.parse( new TextDecoder().decode(response.Payload), ) as ProcessResult; } ``` **Why good:** checks `FunctionError` before parsing payload (Lambda sets this on unhandled exceptions), `TextDecoder` properly decodes the `Uint8Array` payload **Gotcha:** `response.Payload` is a `Uint8Array`, not a string — always decode with `new TextDecoder().decode()` before `JSON.parse()`. --- ## Lambda — Asynchronous Invocation ```typescript export async function triggerAsync( functionName: string, payload: unknown, ): Promise<void> { await lambda.send( new InvokeCommand({ FunctionName: functionName, InvocationType: "Event", // Async — returns immediately, Lambda retries up to 2 times Payload: JSON.stringify(payload), }), ); // Returns 202 Accepted — no response payload } ``` **When to use:** fire-and-forget operations where you don't need the result (notifications, background processing, fan-out). --- ## Secrets Manager — Retrieve Secret ```typescript import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager"; const secretsManager = new SecretsManagerClient({}); export async function getSecret(secretName: string): Promise<string> { const { SecretString } = await secretsManager.send( new GetSecretValueCommand({ SecretId: secretName }), ); if (!SecretString) { throw new Error(`Secret "${secretName}" has no string value`); } return SecretString; } // For JSON secrets (common pattern: DB credentials, API keys) interface DbCredentials { host: string; port: number; username: string; password: string; } export async function getDbCredentials( secretName: string, ): Promise<DbCredentials> { const secretString = await getSecret(secretName); return JSON.parse(secretString) as DbCredentials; } ``` --- ## Secrets Manager — Cached Secret Secrets don't change frequently. Cache them to avoid API calls on every request. ```typescript const SECRET_CACHE_TTL_MS = 300_000; // 5 minutes interface CachedSecret { value: string; expiresAt: number; } const secretCache = new Map<string, CachedSecret>(); export async function getCachedSecret(secretName: string): Promise<string> { const cached = secretCache.get(secretName); if (cached && cached.expiresAt > Date.now()) { return cached.value; } const value = await getSecret(secretName); secretCache.set(secretName, { value, expiresAt: Date.now() + SECRET_CACHE_TTL_MS, }); return value; } ``` **Why good:** avoids Secrets Manager API call on every request, TTL ensures secrets refresh periodically, simple in-memory cache works well in Lambda (cache persists across warm invocations) --- ## S3 — Presigned URLs Presigned URLs grant temporary access to S3 objects without exposing AWS credentials. ```typescript import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { s3 } from "./lib/aws-clients.js"; const DOWNLOAD_EXPIRY_SECONDS = 3_600; // 1 hour const UPLOAD_EXPIRY_SECONDS = 900; // 15 minutes const MAX_UPLOAD_SIZE = 10 * 1024 * 1024; // 10 MB // Generate a download URL export async function getDownloadUrl( bucket: string, key: string, ): Promise<string> { return getSignedUrl(s3, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: DOWNLOAD_EXPIRY_SECONDS, }); } // Generate an upload URL with content-type restriction export async function getUploadUrl( bucket: string, key: string, contentType: string, ): Promise<string> { return getSignedUrl( s3, new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType, ContentLength: MAX_UPLOAD_SIZE, // Enforces max file size }), { expiresIn: UPLOAD_EXPIRY_SECONDS }, ); } ``` **Why good:** separate expiry times for download vs upload, `ContentType` on upload prevents wrong file types, named constants for all limits **Gotcha:** Presigned URL expiry is capped at 7 days. If using temporary credentials (IAM role, STS), the URL stops working when the signing credentials expire — even if `expiresIn` is longer. --- ## S3 — Streaming Large Files For large files, stream directly without buffering the entire content in memory. ```typescript import { GetObjectCommand } from "@aws-sdk/client-s3"; import { createWriteStream } from "node:fs"; import { pipeline } from "node:stream/promises"; import { Readable } from "node:stream"; import { s3 } from "./lib/aws-clients.js"; export async function downloadToFile( bucket: string, key: string, outputPath: string, ): Promise<void> { const response = await s3.send( new GetObjectCommand({ Bucket: bucket, Key: key }), ); if (!response.Body) throw new Error(`Empty response for ${key}`); // Convert web ReadableStream to Node.js Readable const nodeStream = Readable.fromWeb(response.Body.transformToWebStream()); await pipeline(nodeStream, createWriteStream(outputPath)); } ``` **Why good:** `pipeline` handles backpressure and cleanup, no memory buffering for large files, `Readable.fromWeb()` bridges web streams to Node.js streams --- ## Middleware — Custom Request Logging The middleware stack lets you intercept and modify requests at various stages. ```typescript import { S3Client } from "@aws-sdk/client-s3"; const s3 = new S3Client({ region: "us-east-1" }); s3.middlewareStack.add( (next, context) => async (args) => { const startTime = Date.now(); const result = await next(args); const duration = Date.now() - startTime; console.log( JSON.stringify({ service: "s3", operation: context.commandName, durationMs: duration, statusCode: result.response.statusCode, }), ); return result; }, { step: "deserialize", // Run after response is received name: "requestLogger", priority: "low", }, ); ``` **Why good:** middleware runs for every request through this client, structured JSON logging, minimal overhead at the deserialize step --- ## Middleware — Add Custom Headers ```typescript s3.middlewareStack.add( (next) => async (args: any) => { args.request.headers["x-correlation-id"] = getCorrelationId(); return next(args); }, { step: "build", // Run before request is signed name: "correlationId", }, ); ``` **Why good:** adding headers at the `build` step means they get included in request signing, correlation ID enables request tracing across services **Gotcha:** Adding headers at the `finalize` step (after signing) will cause signature mismatch errors for services that verify all headers. --- ## STS — Assume Role for Cross-Account Access ```typescript import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; import { S3Client } from "@aws-sdk/client-s3"; import { fromTemporaryCredentials } from "@aws-sdk/credential-providers"; // Option 1: Using credential provider (recommended — auto-refreshes) const crossAccountS3 = new S3Client({ region: "us-east-1", credentials: fromTemporaryCredentials({ params: { RoleArn: "arn:aws:iam::987654321098:role/data-reader", RoleSessionName: "my-app", }, }), }); // Option 2: Manual AssumeRole (when you need the credentials object) const sts = new STSClient({}); const SESSION_DURATION_SECONDS = 3_600; export async function assumeRole(roleArn: string) { const { Credentials } = await sts.send( new AssumeRoleCommand({ RoleArn: roleArn, RoleSessionName: "my-app", DurationSeconds: SESSION_DURATION_SECONDS, }), ); return { accessKeyId: Credentials!.AccessKeyId!, secretAccessKey: Credentials!.SecretAccessKey!, sessionToken: Credentials!.SessionToken!, expiration: Credentials!.Expiration!, }; } ``` **Why good:** `fromTemporaryCredentials` auto-refreshes when credentials expire, manual option available when you need to pass credentials to external systems -
core.md 9.9 KB
# AWS SDK v3 — Core Patterns > Client setup, S3 operations, DynamoDB basics, credential providers, error handling, and pagination. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Messaging](messaging.md) — SQS, SNS patterns - [Advanced](advanced.md) — Lambda, Secrets Manager, presigned URLs, middleware --- ## Client Setup and Reuse Create clients once at module scope and reuse them. Clients manage connection pooling internally. ```typescript // lib/aws-clients.ts — shared client instances import { S3Client } from "@aws-sdk/client-s3"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; const REGION = process.env.AWS_REGION ?? "us-east-1"; export const s3 = new S3Client({ region: REGION }); const ddbClient = new DynamoDBClient({ region: REGION }); export const ddb = DynamoDBDocumentClient.from(ddbClient, { marshallOptions: { removeUndefinedValues: true }, }); ``` **Why good:** single client instance reuses TCP connections, `removeUndefinedValues` avoids marshalling errors from optional fields, region from env var with fallback ```typescript // BAD: Creating a new client per request async function getUser(userId: string) { const client = new DynamoDBClient({ region: "us-east-1" }); // New connection per call const ddb = DynamoDBDocumentClient.from(client); // ... } ``` **Why bad:** creates new TCP connections per request, wastes resources, slower due to connection setup overhead --- ## S3 Upload ```typescript import { PutObjectCommand } from "@aws-sdk/client-s3"; import { s3 } from "./lib/aws-clients.js"; const CONTENT_TYPE_JSON = "application/json"; export async function uploadJson( bucket: string, key: string, data: unknown, ): Promise<void> { await s3.send( new PutObjectCommand({ Bucket: bucket, Key: key, Body: JSON.stringify(data), ContentType: CONTENT_TYPE_JSON, }), ); } ``` **Why good:** explicit `ContentType` prevents S3 defaulting to `application/octet-stream`, reuses shared client --- ## S3 Download ```typescript import { GetObjectCommand, NoSuchKey } from "@aws-sdk/client-s3"; import { s3 } from "./lib/aws-clients.js"; export async function downloadJson<T>( bucket: string, key: string, ): Promise<T | null> { try { const response = await s3.send( new GetObjectCommand({ Bucket: bucket, Key: key }), ); const body = await response.Body?.transformToString(); if (!body) return null; return JSON.parse(body) as T; } catch (error) { if (error instanceof NoSuchKey) return null; throw error; } } ``` **Why good:** `transformToString()` properly consumes the stream, `instanceof NoSuchKey` gives typed error handling, returns `null` for missing keys instead of throwing ```typescript // BAD: v2-style error handling try { await s3.send(new GetObjectCommand({ Bucket: "b", Key: "k" })); } catch (error: any) { if (error.code === "NoSuchKey") { // String comparison — fragile, no type narrowing return null; } } ``` **Why bad:** `.code` is a v2 pattern, no TypeScript narrowing, `any` type loses safety --- ## S3 Delete ```typescript import { DeleteObjectCommand } from "@aws-sdk/client-s3"; import { s3 } from "./lib/aws-clients.js"; export async function deleteObject(bucket: string, key: string): Promise<void> { await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })); // Note: DeleteObject succeeds even if the key doesn't exist (idempotent) } ``` --- ## S3 List with Pagination ```typescript import { paginateListObjectsV2 } from "@aws-sdk/client-s3"; import { s3 } from "./lib/aws-clients.js"; const MAX_KEYS_PER_PAGE = 1_000; export async function listAllKeys( bucket: string, prefix: string, ): Promise<string[]> { const paginator = paginateListObjectsV2( { client: s3, pageSize: MAX_KEYS_PER_PAGE }, { Bucket: bucket, Prefix: prefix }, ); const keys: string[] = []; for await (const page of paginator) { const pageKeys = page.Contents?.map((obj) => obj.Key).filter(Boolean) ?? []; keys.push(...(pageKeys as string[])); } return keys; } ``` **Why good:** built-in paginator handles continuation tokens automatically, `for await...of` is clean and readable, `pageSize` controls batch size ```typescript // BAD: Manual pagination with token tracking let token: string | undefined; const keys: string[] = []; do { const response = await s3.send( new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix, ContinuationToken: token, }), ); keys.push(...(response.Contents?.map((o) => o.Key!).filter(Boolean) ?? [])); token = response.NextContinuationToken; } while (token); ``` **Why bad:** manual token tracking is error-prone and verbose, built-in paginators exist for this exact purpose --- ## DynamoDB Get ```typescript import { GetCommand } from "@aws-sdk/lib-dynamodb"; import { ddb } from "./lib/aws-clients.js"; interface User { userId: string; email: string; name: string; createdAt: string; } export async function getUser(userId: string): Promise<User | null> { const { Item } = await ddb.send( new GetCommand({ TableName: "users", Key: { userId }, }), ); return (Item as User) ?? null; } ``` **Why good:** `GetCommand` from `@aws-sdk/lib-dynamodb` accepts native JS objects (no marshalling), returns native JS objects --- ## DynamoDB Put ```typescript import { PutCommand } from "@aws-sdk/lib-dynamodb"; import { ddb } from "./lib/aws-clients.js"; export async function createUser(user: User): Promise<void> { await ddb.send( new PutCommand({ TableName: "users", Item: user, ConditionExpression: "attribute_not_exists(userId)", // Prevent overwrite }), ); } ``` --- ## DynamoDB Query ```typescript import { QueryCommand } from "@aws-sdk/lib-dynamodb"; import { ddb } from "./lib/aws-clients.js"; const DEFAULT_LIMIT = 20; export async function getUserOrders( userId: string, limit = DEFAULT_LIMIT, ): Promise<Order[]> { const { Items } = await ddb.send( new QueryCommand({ TableName: "orders", KeyConditionExpression: "userId = :userId", ExpressionAttributeValues: { ":userId": userId }, ScanIndexForward: false, // newest first Limit: limit, }), ); return (Items as Order[]) ?? []; } ``` --- ## DynamoDB Update ```typescript import { UpdateCommand } from "@aws-sdk/lib-dynamodb"; import { ddb } from "./lib/aws-clients.js"; export async function updateUserName( userId: string, name: string, ): Promise<void> { await ddb.send( new UpdateCommand({ TableName: "users", Key: { userId }, UpdateExpression: "SET #name = :name, updatedAt = :now", ExpressionAttributeNames: { "#name": "name" }, // "name" is a DynamoDB reserved word ExpressionAttributeValues: { ":name": name, ":now": new Date().toISOString(), }, ConditionExpression: "attribute_exists(userId)", // Fail if user doesn't exist }), ); } ``` **Why good:** `ExpressionAttributeNames` handles the reserved word "name", `ConditionExpression` prevents updating non-existent items, timestamps updated atomically --- ## DynamoDB Delete ```typescript import { DeleteCommand } from "@aws-sdk/lib-dynamodb"; import { ddb } from "./lib/aws-clients.js"; export async function deleteUser(userId: string): Promise<void> { await ddb.send( new DeleteCommand({ TableName: "users", Key: { userId }, ConditionExpression: "attribute_exists(userId)", // Fail if already deleted }), ); } ``` --- ## Credential Providers ```typescript import { S3Client } from "@aws-sdk/client-s3"; import { fromIni, fromTemporaryCredentials, } from "@aws-sdk/credential-providers"; // Local development — use a named AWS profile const devClient = new S3Client({ region: "us-east-1", credentials: fromIni({ profile: "my-dev-profile" }), }); // Cross-account access — assume a role in another account const crossAccountClient = new S3Client({ region: "us-east-1", credentials: fromTemporaryCredentials({ params: { RoleArn: "arn:aws:iam::123456789012:role/cross-account-role", RoleSessionName: "my-app-session", }, }), }); ``` **Why good:** explicit credential providers for specific use cases, no hardcoded keys, `fromTemporaryCredentials` uses STS AssumeRole under the hood **Note:** In Lambda, ECS, and EC2, don't configure credentials at all — the default chain picks up the IAM role automatically. --- ## Error Handling — Full Pattern ```typescript import { GetObjectCommand, NoSuchKey, NoSuchBucket, S3ServiceException, } from "@aws-sdk/client-s3"; import { s3 } from "./lib/aws-clients.js"; export async function safeGetObject( bucket: string, key: string, ): Promise<string | null> { try { const response = await s3.send( new GetObjectCommand({ Bucket: bucket, Key: key }), ); return (await response.Body?.transformToString()) ?? null; } catch (error) { // 1. Check specific exception types first if (error instanceof NoSuchKey) { return null; // Expected — object doesn't exist } if (error instanceof NoSuchBucket) { throw new Error(`Bucket "${bucket}" does not exist`); } // 2. Check general service exception if (error instanceof S3ServiceException) { // Access $metadata for HTTP status and request ID const { httpStatusCode, requestId } = error.$metadata; throw new Error( `S3 error [${httpStatusCode}]: ${error.message} (requestId: ${requestId})`, ); } // 3. Non-AWS error (network timeout, DNS failure, etc.) throw error; } } ``` --- ## Retry Configuration ```typescript import { S3Client } from "@aws-sdk/client-s3"; const MAX_RETRY_ATTEMPTS = 5; const s3 = new S3Client({ region: "us-east-1", maxAttempts: MAX_RETRY_ATTEMPTS, // Default is 3 }); ``` The SDK automatically retries throttling errors (429) and transient server errors (500, 502, 503) with exponential backoff. Increase `maxAttempts` for high-throughput workloads. -
messaging.md 6.1 KB
# AWS SDK v3 — Messaging Patterns (SQS & SNS) > SQS send/receive/delete, SNS publish, FIFO queues, and dead-letter patterns. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Core Patterns](core.md) — Client setup, S3, DynamoDB, error handling - [Advanced](advanced.md) — Lambda, Secrets Manager, presigned URLs, middleware --- ## SQS — Send Message ```typescript import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; const sqs = new SQSClient({}); export async function enqueueOrder( queueUrl: string, order: OrderPayload, ): Promise<string> { const result = await sqs.send( new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: JSON.stringify(order), MessageAttributes: { eventType: { DataType: "String", StringValue: "order.created" }, }, }), ); return result.MessageId!; } ``` **Why good:** message attributes enable filtering at the subscription level, typed `SendMessageCommand` provides input validation --- ## SQS — Receive and Delete Messages Always delete messages after successful processing. Undeleted messages reappear after the visibility timeout. ```typescript import { ReceiveMessageCommand, DeleteMessageCommand, } from "@aws-sdk/client-sqs"; const MAX_MESSAGES = 10; const WAIT_TIME_SECONDS = 20; // Long polling — reduces empty responses and cost export async function pollMessages(queueUrl: string): Promise<void> { const { Messages } = await sqs.send( new ReceiveMessageCommand({ QueueUrl: queueUrl, MaxNumberOfMessages: MAX_MESSAGES, WaitTimeSeconds: WAIT_TIME_SECONDS, MessageAttributeNames: ["All"], }), ); // IMPORTANT: Messages may be undefined (not empty array) when queue is empty for (const message of Messages ?? []) { try { const body = JSON.parse(message.Body!); await processMessage(body); // Delete only after successful processing await sqs.send( new DeleteMessageCommand({ QueueUrl: queueUrl, ReceiptHandle: message.ReceiptHandle!, }), ); } catch (error) { // Message stays in queue — will be retried after visibility timeout console.error(`Failed to process message ${message.MessageId}:`, error); } } } ``` **Why good:** long polling (`WaitTimeSeconds: 20`) reduces empty responses and API costs, per-message try/catch prevents one failure from blocking the batch, delete-after-process ensures at-least-once delivery ```typescript // BAD: Deleting before processing for (const message of Messages ?? []) { await sqs.send( new DeleteMessageCommand({ QueueUrl: queueUrl, ReceiptHandle: message.ReceiptHandle!, }), ); await processMessage(JSON.parse(message.Body!)); // If this fails, message is lost } ``` **Why bad:** deleting before processing means failed messages are lost permanently — they never return to the queue --- ## SQS — FIFO Queue FIFO queues guarantee ordering and exactly-once processing within a message group. ```typescript const FIFO_QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/orders.fifo"; export async function enqueueFifoMessage( orderId: string, payload: unknown, ): Promise<void> { await sqs.send( new SendMessageCommand({ QueueUrl: FIFO_QUEUE_URL, MessageBody: JSON.stringify(payload), MessageGroupId: orderId, // Messages with same group ID are ordered MessageDeduplicationId: `${orderId}-${Date.now()}`, // Prevents duplicate processing }), ); } ``` **Why good:** `MessageGroupId` ensures all messages for the same order are processed in order, `MessageDeduplicationId` prevents duplicate delivery within the 5-minute deduplication window **Gotcha:** FIFO queue URLs must end with `.fifo`. The `MessageGroupId` and `MessageDeduplicationId` are required for FIFO queues. --- ## SQS — Batch Send Use `SendMessageBatchCommand` to send up to 10 messages in a single API call. ```typescript import { SendMessageBatchCommand } from "@aws-sdk/client-sqs"; const MAX_BATCH_SIZE = 10; export async function enqueueBatch( queueUrl: string, messages: OrderPayload[], ): Promise<void> { // Split into chunks of MAX_BATCH_SIZE for (let i = 0; i < messages.length; i += MAX_BATCH_SIZE) { const batch = messages.slice(i, i + MAX_BATCH_SIZE); await sqs.send( new SendMessageBatchCommand({ QueueUrl: queueUrl, Entries: batch.map((msg, idx) => ({ Id: String(idx), MessageBody: JSON.stringify(msg), })), }), ); } } ``` --- ## SNS — Publish to Topic ```typescript import { SNSClient, PublishCommand } from "@aws-sdk/client-sns"; const sns = new SNSClient({}); export async function publishEvent( topicArn: string, event: { type: string; payload: unknown }, ): Promise<string> { const result = await sns.send( new PublishCommand({ TopicArn: topicArn, Message: JSON.stringify(event.payload), MessageAttributes: { eventType: { DataType: "String", StringValue: event.type }, }, }), ); return result.MessageId!; } ``` **Why good:** message attributes enable SNS subscription filter policies — subscribers only receive events matching their filter --- ## SNS — Publish with Subject (for Email Subscriptions) ```typescript export async function sendNotification( topicArn: string, subject: string, message: string, ): Promise<void> { await sns.send( new PublishCommand({ TopicArn: topicArn, Subject: subject, // Used as email subject for email subscribers Message: message, }), ); } ``` --- ## SNS — Publish to FIFO Topic ```typescript const FIFO_TOPIC_ARN = "arn:aws:sns:us-east-1:123456789012:orders.fifo"; export async function publishFifoEvent( groupId: string, payload: unknown, ): Promise<void> { await sns.send( new PublishCommand({ TopicArn: FIFO_TOPIC_ARN, Message: JSON.stringify(payload), MessageGroupId: groupId, MessageDeduplicationId: `${groupId}-${Date.now()}`, }), ); } ``` **Gotcha:** FIFO topic ARNs must end with `.fifo`. FIFO topics can only deliver to FIFO SQS queues, not standard queues or other endpoint types.
-
-
reference.md 5.1 KB
# AWS SDK v3 Quick Reference ## Package Cheat Sheet | Service | Client Package | Key Commands | | --------------- | --------------------------------- | --------------------------------------------------------- | | S3 | `@aws-sdk/client-s3` | `PutObject`, `GetObject`, `DeleteObject`, `ListObjectsV2` | | S3 Presigning | `@aws-sdk/s3-request-presigner` | `getSignedUrl` | | DynamoDB (raw) | `@aws-sdk/client-dynamodb` | `GetItem`, `PutItem`, `Query`, `Scan`, `UpdateItem` | | DynamoDB (doc) | `@aws-sdk/lib-dynamodb` | `Get`, `Put`, `Query`, `Scan`, `Update`, `Delete` | | SQS | `@aws-sdk/client-sqs` | `SendMessage`, `ReceiveMessage`, `DeleteMessage` | | SNS | `@aws-sdk/client-sns` | `Publish`, `Subscribe`, `CreateTopic` | | Lambda | `@aws-sdk/client-lambda` | `Invoke`, `InvokeAsync` | | Secrets Manager | `@aws-sdk/client-secrets-manager` | `GetSecretValue`, `CreateSecret`, `UpdateSecret` | | STS | `@aws-sdk/client-sts` | `AssumeRole`, `GetCallerIdentity` | | Credentials | `@aws-sdk/credential-providers` | `fromEnv`, `fromIni`, `fromTemporaryCredentials` | | DynamoDB Utils | `@aws-sdk/util-dynamodb` | `marshall`, `unmarshall` (only if using raw client) | --- ## Import Pattern ```typescript // Client + commands from the same package import { S3Client, PutObjectCommand, GetObjectCommand, } from "@aws-sdk/client-s3"; // Exception classes also exported from client package import { NoSuchKey, S3ServiceException } from "@aws-sdk/client-s3"; // Types use `import type` import type { PutObjectCommandInput, GetObjectCommandOutput, } from "@aws-sdk/client-s3"; // Presigning is a separate package import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; // DynamoDB Document Client — commands from lib-dynamodb, NOT client-dynamodb import { DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, } from "@aws-sdk/lib-dynamodb"; // Credential providers import { fromIni, fromTemporaryCredentials, } from "@aws-sdk/credential-providers"; ``` --- ## Error Handling Decision Tree ``` Caught an error from client.send()? | +-- Is it a specific exception you expect? | +-- YES --> instanceof SpecificException (e.g., NoSuchKey, ConditionalCheckFailedException) | Access: error.name, error.message, error.$metadata | +-- Is it any service error? | +-- YES --> instanceof ServiceException (e.g., S3ServiceException) | Check: error.$metadata.httpStatusCode | +-- Is it a network/timeout error? +-- YES --> Likely not an AWS exception, check error.message +-- Rethrow if unrecognized ``` --- ## Credential Provider Chain (Default Order) When no explicit credentials are configured, the SDK resolves credentials in this order: 1. **Environment variables** — `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN` 2. **SSO credentials** — `~/.aws/sso/cache/` 3. **Shared credentials file** — `~/.aws/credentials` (default profile or `AWS_PROFILE`) 4. **ECS container credentials** — `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` 5. **EC2 instance metadata** — IMDSv2 role credentials 6. **SSO token provider** — If configured in `~/.aws/config` In Lambda, ECS, and EC2 the default chain resolves automatically via IAM roles — no configuration needed. --- ## Client Configuration Options ```typescript const client = new S3Client({ region: "us-east-1", // Required (or set AWS_REGION env var) credentials: fromIni({ profile: "dev" }), // Explicit provider (optional) maxAttempts: 5, // Retry attempts (default: 3) requestHandler: new NodeHttpHandler({ // Custom HTTP settings connectionTimeout: 5_000, socketTimeout: 10_000, }), logger: console, // SDK debug logging }); ``` --- ## DynamoDB Expression Patterns | Operation | Expression | | -------------------- | ----------------------------------------------------------------- | | Query by PK | `KeyConditionExpression: "pk = :pk"` | | Query PK + SK prefix | `KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)"` | | Filter results | `FilterExpression: "status = :status"` | | Update attribute | `UpdateExpression: "SET #name = :name"` | | Increment counter | `UpdateExpression: "SET viewCount = viewCount + :inc"` | | Remove attribute | `UpdateExpression: "REMOVE deletedAt"` | | Conditional write | `ConditionExpression: "attribute_not_exists(pk)"` | **Expression attribute names** (`#name`) are required when the attribute name is a DynamoDB reserved word. **Expression attribute values** (`:value`) are always required in expressions. -
SKILL.md 16 KB
--- name: infra-platform-aws-sdk description: AWS SDK v3 for TypeScript — modular clients, command pattern, S3, DynamoDB, SQS, Lambda, SNS, Secrets Manager --- # AWS SDK v3 Patterns > **Quick Guide:** AWS SDK v3 for JavaScript/TypeScript uses modular packages (`@aws-sdk/client-*`) with a command pattern: create a client, instantiate a command, call `client.send(command)`. Import only the services you need for tree-shaking. Use `DynamoDBDocumentClient` for native JS types. Use `getSignedUrl` from `@aws-sdk/s3-request-presigner` for presigned URLs. Handle errors with `instanceof` specific exception classes. Use built-in paginators (`paginate*`) with `for await...of`. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use AWS SDK v3 modular packages (`@aws-sdk/client-*`) — NEVER the monolithic `aws-sdk` v2 package)** **(You MUST use the command pattern: `client.send(new XxxCommand({...}))` — NEVER call methods directly on the client)** **(You MUST use `DynamoDBDocumentClient` from `@aws-sdk/lib-dynamodb` for DynamoDB — it auto-marshalls native JS types)** **(You MUST handle errors with `instanceof` specific exception classes — NEVER catch generic `Error` and check `.code`)** **(You MUST use built-in paginators (`paginate*` functions) for paginated APIs — NEVER manually track continuation tokens)** </critical_requirements> --- ## Examples - [Core Patterns](examples/core.md) — Client setup, S3 operations, DynamoDB basics, credential providers, error handling, pagination - [Messaging](examples/messaging.md) — SQS send/receive/delete, SNS publish, FIFO queues, dead-letter patterns - [Advanced](examples/advanced.md) — Lambda invocation, Secrets Manager, presigned URLs, middleware, streaming - [Quick Reference](reference.md) — Package cheat sheet, import patterns, error handling decision tree, credential provider chain --- **Auto-detection:** AWS SDK, @aws-sdk/client, S3Client, DynamoDBClient, DynamoDBDocumentClient, SQSClient, LambdaClient, SNSClient, SecretsManagerClient, PutObjectCommand, GetObjectCommand, GetCommand, PutCommand, QueryCommand, SendMessageCommand, InvokeCommand, GetSecretValueCommand, getSignedUrl, s3-request-presigner, credential-providers, fromEnv, fromIni, paginateListObjectsV2, aws-sdk-client-mock **When to use:** - Interacting with any AWS service from TypeScript/JavaScript - S3 file operations (upload, download, presigned URLs, listings) - DynamoDB CRUD operations and queries - SQS message sending, receiving, and queue management - Lambda function invocation from other services - SNS topic publishing and notifications - Secrets Manager secret retrieval - Custom middleware for request/response modification **When NOT to use:** - Infrastructure provisioning (use an IaC tool) - AWS console-only operations with no SDK equivalent - Simple CLI-only tasks better served by the AWS CLI directly **Key patterns covered:** - Modular client setup with typed configuration - Command pattern (`client.send(new Command({...}))`) - S3: upload, download, delete, list, presigned URLs, streaming - DynamoDB: `DynamoDBDocumentClient` with `Get`, `Put`, `Query`, `Update`, `Delete` - SQS: send, receive, delete messages, long polling, FIFO - SNS: publish to topics, message attributes - Lambda: synchronous and asynchronous invocation - Secrets Manager: secret retrieval with caching - Credential provider chain and explicit providers - Error handling with `instanceof` exception classes and `$metadata` - Pagination with async iterators - Middleware stack customization - Retry configuration --- <philosophy> ## Philosophy AWS SDK v3 is a ground-up rewrite of the v2 SDK for modern JavaScript/TypeScript. The core design principles: 1. **Modular packages** — Each service is a separate npm package (`@aws-sdk/client-s3`, `@aws-sdk/client-dynamodb`). Import only what you use. This reduces bundle size by up to 90% compared to the monolithic v2 `aws-sdk` package. 2. **Command pattern** — Every API call is a Command object sent through a Client. This enables middleware, type safety, and testability. The client handles serialization, signing, retries, and deserialization. 3. **First-class TypeScript** — Every command input and output is fully typed. Use the types to avoid runtime errors. 4. **Middleware stack** — Customize request/response handling at various stages (serialize, build, finalize, deserialize) without monkey-patching. 5. **Built-in pagination** — Paginator functions return async iterators, eliminating manual token tracking. **When to use AWS SDK v3:** - Any server-side or serverless TypeScript/JavaScript that interacts with AWS services - Frontend applications that need direct AWS access (with appropriate auth) - Lambda functions (SDK v3 is included in Node.js 18+ Lambda runtimes) **When NOT to use:** - Infrastructure provisioning and management (use an IaC tool) - One-off tasks better served by the AWS CLI - Languages other than JavaScript/TypeScript </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Client Setup and Command Pattern Every AWS service follows the same pattern: import the client and command, create a client instance, send the command. ```typescript import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; const s3 = new S3Client({ region: "us-east-1" }); await s3.send( new PutObjectCommand({ Bucket: "my-bucket", Key: "data.json", Body: JSON.stringify({ hello: "world" }), ContentType: "application/json", }), ); ``` **Why good:** modular import keeps bundle small, command pattern enables middleware and type safety, region is explicit Create clients once and reuse them — they manage connection pooling internally. In Lambda, create clients outside the handler for connection reuse across invocations. See [examples/core.md](examples/core.md) for client reuse patterns and configuration options. --- ### Pattern 2: S3 Operations S3 is the most commonly used service. Key operations: `PutObject`, `GetObject`, `DeleteObject`, `ListObjectsV2`, and presigned URLs via `@aws-sdk/s3-request-presigner`. ```typescript import { GetObjectCommand, NoSuchKey } from "@aws-sdk/client-s3"; const response = await s3.send( new GetObjectCommand({ Bucket: "my-bucket", Key: "data.json", }), ); const body = await response.Body?.transformToString(); ``` For presigned URLs, use the separate `@aws-sdk/s3-request-presigner` package: ```typescript import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const PRESIGN_EXPIRY_SECONDS = 3_600; const url = await getSignedUrl( s3, new GetObjectCommand({ Bucket: "my-bucket", Key: "file.pdf", }), { expiresIn: PRESIGN_EXPIRY_SECONDS }, ); ``` See [examples/core.md](examples/core.md) for upload, download, delete, list, and streaming patterns. --- ### Pattern 3: DynamoDB with Document Client Use `DynamoDBDocumentClient` from `@aws-sdk/lib-dynamodb` — it automatically marshalls/unmarshalls between native JS types and DynamoDB's `AttributeValue` format. ```typescript import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, GetCommand, PutCommand, } from "@aws-sdk/lib-dynamodb"; const ddbDocClient = DynamoDBDocumentClient.from(new DynamoDBClient({})); const { Item } = await ddbDocClient.send( new GetCommand({ TableName: "users", Key: { userId: "abc-123" }, }), ); ``` **Why good:** no manual `marshall()`/`unmarshall()` calls, native JS objects in and out, full type safety **Gotcha:** Import commands from `@aws-sdk/lib-dynamodb` (not `@aws-sdk/client-dynamodb`) when using the document client — the lib-dynamodb commands accept native JS types. See [examples/core.md](examples/core.md) for Put, Query, Update, Delete, and batch operations. --- ### Pattern 4: Error Handling AWS SDK v3 errors extend service-specific base classes (e.g., `S3ServiceException`). Use `instanceof` for typed error handling. ```typescript import { GetObjectCommand, NoSuchKey, S3ServiceException, } from "@aws-sdk/client-s3"; try { await s3.send(new GetObjectCommand({ Bucket: "b", Key: "k" })); } catch (error) { if (error instanceof NoSuchKey) { // Typed: error.name === "NoSuchKey", error.$metadata.httpStatusCode === 404 return null; } if (error instanceof S3ServiceException) { // Any S3 service error — check error.$metadata.httpStatusCode throw error; } throw error; // Non-AWS error (network, etc.) } ``` **Why good:** `instanceof` gives TypeScript type narrowing, exception classes are exported from the client package, `$metadata` provides HTTP status and request ID for debugging See [examples/core.md](examples/core.md) for the full error handling decision tree and retry patterns. --- ### Pattern 5: Pagination with Async Iterators Use built-in paginator functions for any paginated API. They return async iterators that handle continuation tokens automatically. ```typescript import { paginateListObjectsV2 } from "@aws-sdk/client-s3"; for await (const page of paginateListObjectsV2( { client: s3 }, { Bucket: "my-bucket" }, )) { // page.Contents is an array of objects for this page } ``` **Why good:** no manual token tracking, clean `for await...of` loop, handles all edge cases (empty pages, token format) See [examples/core.md](examples/core.md) for full S3 list, DynamoDB pagination, and good/bad comparison with manual token tracking. --- ### Pattern 6: SQS Messaging SQS uses `SendMessageCommand`, `ReceiveMessageCommand`, and `DeleteMessageCommand`. Always delete messages after processing. ```typescript import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; const sqs = new SQSClient({}); await sqs.send( new SendMessageCommand({ QueueUrl: QUEUE_URL, MessageBody: JSON.stringify({ orderId: "order-123" }), }), ); ``` See [examples/messaging.md](examples/messaging.md) for receive/delete, long polling, FIFO queues, and dead-letter patterns. --- ### Pattern 7: SNS Publishing SNS publishes messages to topics. Subscribers receive messages on their configured endpoints. ```typescript import { SNSClient, PublishCommand } from "@aws-sdk/client-sns"; const sns = new SNSClient({}); await sns.send( new PublishCommand({ TopicArn: TOPIC_ARN, Message: JSON.stringify({ event: "order.created", orderId: "order-123" }), MessageAttributes: { eventType: { DataType: "String", StringValue: "order.created" }, }, }), ); ``` See [examples/messaging.md](examples/messaging.md) for topic management and message filtering. --- ### Pattern 8: Lambda Invocation and Secrets Manager Invoke Lambda functions synchronously or asynchronously. Retrieve secrets from Secrets Manager with caching. ```typescript import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; const lambda = new LambdaClient({}); const response = await lambda.send( new InvokeCommand({ FunctionName: "process-order", InvocationType: "RequestResponse", // synchronous Payload: JSON.stringify({ orderId: "order-123" }), }), ); const result = JSON.parse(new TextDecoder().decode(response.Payload)); ``` See [examples/advanced.md](examples/advanced.md) for async invocation, Secrets Manager retrieval, and caching patterns. </patterns> --- <decision_framework> ## Decision Framework ### Choosing the Right DynamoDB Client ``` Are you working with DynamoDB? | +-- Need native JS objects (recommended) --> DynamoDBDocumentClient from @aws-sdk/lib-dynamodb | +-- Import Get/Put/Query/Update/Delete Commands from @aws-sdk/lib-dynamodb | +-- Need raw AttributeValue format --> DynamoDBClient from @aws-sdk/client-dynamodb +-- Import commands from @aws-sdk/client-dynamodb +-- Manually marshall/unmarshall with @aws-sdk/util-dynamodb ``` ### Choosing Between Synchronous and Async Lambda Invocation ``` Do you need the Lambda response immediately? | +-- YES --> InvocationType: "RequestResponse" (synchronous, waits for result) | +-- NO --> InvocationType: "Event" (async, returns immediately, 3 retries) ``` ### Credential Provider Selection ``` Where is this code running? | +-- Lambda / ECS / EC2 --> Default chain (auto-detects IAM role) — no config needed | +-- Local development --> fromIni() (reads ~/.aws/credentials) or fromEnv() | +-- CI/CD pipeline --> fromEnv() with AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | +-- Cross-account access --> fromTemporaryCredentials() with STS AssumeRole ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Using the monolithic `aws-sdk` v2 package — it ships the entire SDK (~70 MB). Use modular `@aws-sdk/client-*` packages. - Calling methods directly on the client (v2 style: `s3.getObject()`) — use the command pattern: `s3.send(new GetObjectCommand({...}))`. - Using raw `DynamoDBClient` commands with manual `marshall()`/`unmarshall()` — use `DynamoDBDocumentClient` from `@aws-sdk/lib-dynamodb`. - Catching errors with `.code` string comparison (v2 style) — use `instanceof` with typed exception classes. - Manually tracking pagination tokens in a while loop — use built-in `paginate*` functions with `for await...of`. - Hardcoding AWS credentials in source code — use the credential provider chain or environment variables. **Medium Priority Issues:** - Creating a new client instance per request — create clients once and reuse them (they manage connection pooling). - Not setting a region explicitly — defaults vary by environment and cause confusing errors. - Mixing `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb` command imports — pick one approach per codebase. - Missing `ContentType` on S3 `PutObject` — S3 defaults to `application/octet-stream`, breaking browser downloads. - Not buffering the S3 `GetObject` response body — `response.Body` is a stream; call `.transformToString()` or `.transformToByteArray()`. **Gotchas and Edge Cases:** - `GetObject` response body is a `ReadableStream` (not a string) — you must consume it with `.transformToString()`, `.transformToByteArray()`, or pipe it to a writable stream. - `DynamoDBDocumentClient` commands come from `@aws-sdk/lib-dynamodb`, NOT `@aws-sdk/client-dynamodb` — importing from the wrong package gives you raw `AttributeValue` types. - Presigned URLs require the separate `@aws-sdk/s3-request-presigner` package — it is NOT included in `@aws-sdk/client-s3`. - `InvokeCommand` returns `Payload` as a `Uint8Array` — decode with `new TextDecoder().decode(response.Payload)` before `JSON.parse`. - SDK v3 version mismatches across client packages cause TypeScript errors — pin all `@aws-sdk/*` packages to the same version range. - In Lambda, the SDK is bundled in the runtime but may be outdated — bundle your own version for latest features. - `SQS ReceiveMessageCommand` may return `Messages: undefined` (not empty array) when no messages are available — always use `response.Messages ?? []`. - Presigned URL expiry is capped at 7 days, but temporary credentials may expire sooner — the URL stops working when the signing credentials expire. </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST use AWS SDK v3 modular packages (`@aws-sdk/client-*`) — NEVER the monolithic `aws-sdk` v2 package)** **(You MUST use the command pattern: `client.send(new XxxCommand({...}))` — NEVER call methods directly on the client)** **(You MUST use `DynamoDBDocumentClient` from `@aws-sdk/lib-dynamodb` for DynamoDB — it auto-marshalls native JS types)** **(You MUST handle errors with `instanceof` specific exception classes — NEVER catch generic `Error` and check `.code`)** **(You MUST use built-in paginators (`paginate*` functions) for paginated APIs — NEVER manually track continuation tokens)** **Failure to follow these rules will cause bloated bundles (v2), lost type safety (direct calls), marshalling bugs (raw DynamoDB), and fragile error handling (string comparison).** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.