Claude Skill

infra-platform-aws-sdk

AWS SDK v3 for TypeScript — modular clients, command pattern, S3, DynamoDB, SQS, Lambda, SNS, Secrets Manager

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_infra-platform-aws-sdk_skills_infra-platform-aws-sdk-3a51ef5.zip · 16 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-platform-aws-sdk/skills/infra-platform-aws-sdk
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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, 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 — 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: 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



<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>

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.

No comments yet.

Reviews (0)

No reviews yet.

Related