DEV Community

dhaldankar
dhaldankar

Posted on

Setting Up Local Lambda Debug Loop with AWS Toolkit and Esbuild

When building cloud-native applications with AWS CDK and TypeScript, local debugging often introduces unnecessary friction. Standard documentation such as the AWS Toolkit Lambda Invocation Guide covers the baseline mechanics of wiring an IDE to the AWS Serverless Application Model (SAM) CLI. However, applying those patterns to a multi-function monorepo exposes distinct bottlenecks: the 60-second cdk synth time, manual asset hash updates in launch.json, and slow feedback loops.

This post breaks down how we restructured our local debugging pipeline to achieve sub-second builds, dynamic handler selection, and seamless TypeScript source-map resolution without touching production code paths.


1. Project Architecture & CDK Structure

Our service lives inside a monorepo workspace under the healthscribe/ directory. The codebase is organized around single-purpose AWS Lambda functions backed by isolated TypeScript handlers:

healthscribe/
├── cdk.out/                  # CDK synthesis output (gitignored)
│   └── debug-latest/         # Static target for local debugging
├── src/
│   ├── handlers/             # Lambda entry points (e.g., start-user-enrollment.ts)
│   └── services/             # Core business logic and repository layers
├── lib/                      # CDK Stack definitions (NodejsFunction constructs)
└── package.json

Enter fullscreen mode Exit fullscreen mode

In our CDK stack definitions (lib/), we use NodejsFunction to bundle handlers via esbuild. For non-production environments, we configure source maps explicitly to enable IDE breakpoint mapping without incurring production overhead:

const handler = new nodejs.NodejsFunction(this, "Function", {
    entry: "src/handlers/start-user-enrollment.ts",
    bundling: {
        minify: false, // Default is false; omitted for clarity
        sourceMap: props.environment !== "prod",
    },
});

Enter fullscreen mode Exit fullscreen mode

2. The Bottleneck: Why Default SAM Workflows Fail at Scale

Out of the box, standard AWS Toolkit configurations rely on target: "code" pointing to synthesized asset folders (e.g., cdk.out/asset.abc123xyz/) or target: "template" relying on CloudFormation logical IDs.

This creates three primary pain points:

  1. The Synth Tax: Running cdk synth bundles every function in the stack sequentially, taking upwards of 60 seconds even if you only changed a single line in one handler.
  2. Hash Drift: Every synth generates a new cryptographic asset hash, forcing developers to manually update launch.json paths constantly.
  3. Template Friction: Pointing the toolkit directly at a CDK-generated CloudFormation template often fails because the extension expects standard AWS SAM template syntax (AWS::Serverless::Function) rather than raw CDK metadata assets.

3. Our Solution: Decoupled Esbuild and Dynamic Handlers

To fix this, we decoupled the build step from the debugger. Instead of letting CDK control the bundle path, we direct esbuild to write directly to a static staging directory (cdk.out/debug-latest/), backed by a prompt-driven VS Code task.

Step 1: Independent Build Task with Handler Prompting (.vscode/tasks.json)

We configured an on-demand task that prompts for the target handler file, bundles it instantly via esbuild, and outputs a fresh bundle complete with source maps:

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Bundle Lambda (One-Off)",
      "type": "shell",
      "options": {
        "cwd": "${workspaceFolder}/healthscribe"
      },
      "command": "npx esbuild ${input:handlerFile} --bundle --platform=node --target=node22 --sourcemap --outfile=cdk.out/debug-latest/index.js",
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": []
    }
  ],
  "inputs": [
    {
      "id": "handlerFile",
      "type": "promptString",
      "description": "Handler .ts entry point (relative to healthscribe/)",
      "default": "src/handlers/start-user-enrollment.ts"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

Step 2: Clean, Decoupled Debugger Configuration (.vscode/launch.json)

Because the build task writes to a predictable static path (cdk.out/debug-latest), our launch.json requires zero asset hash maintenance and no pre-launch coupling. It simply attaches SAM to the pre-built code artifact:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "aws-sam",
      "request": "direct-invoke",
      "name": "Debug Bundled Lambda (cdk.out/debug-latest)",
      "invokeTarget": {
        "target": "code",
        "projectRoot": "${workspaceFolder}/healthscribe/cdk.out/debug-latest",
        "lambdaHandler": "index.handler"
      },
      "lambda": {
        "runtime": "nodejs22.x",
        "memoryMb": 256,
        "timeoutSec": 60,
        "payload": {
          "path": "${workspaceFolder}/healthscribe/${input:eventFile}"
        },
        "environmentVariables": {
          "SCRIBE_TABLE_NAME": "devendra-platform-scribe",
          "CF_CONFIG_TABLE_NAME": "cf-config-devendra-515966531825",
          "UPLOAD_BUCKET": "ntstscribe-upload-devendra-515966531825",
          "TRANSCRIPT_BUCKET": "ntstscribe-transcribe-devendra-515966531825",
          "VOICE_REGISTRATION_API_URL": "https://httpbin.org/status/200",
          "VOICE_REGISTRATION_API_TIMEOUT_MS": "10000"
        }
      },
      "aws": {
        "region": "us-east-1",
        "credentials": "profile:default"
      }
    }
  ],
  "inputs": [
    {
      "id": "eventFile",
      "type": "promptString",
      "description": "Test event .json file (relative to healthscribe/)",
      "default": "event-voice-enrollment-completion.json"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

Summary of Benefits

  • Zero Synth Wait Times: Esbuild compiles single-file entry points in milliseconds instead of waiting a full minute for CDK to synthesize the entire graph.
  • No Stale Hashes: Pointing SAM at cdk.out/debug-latest eliminates manual launch.json edits.
  • First-Class Source Maps: Breakpoints set in src/**/*.ts map cleanly back to the compiled output without runtime overhead in production.

Top comments (0)