{"slug": "how-to-build-a-lightning-fast-typescript-lambda-that-calls-claude-using-esbuild", "title": "How to Build a Lightning‑Fast TypeScript Lambda that Calls Claude, Using esbuild and tsc --noEmit", "summary": "A developer outlined a build pipeline that pairs esbuild with tsc --noEmit to cut TypeScript Lambda deployment times while preserving full type safety. The approach strips type-only imports from the final bundle and produces a Lambda handler that calls Anthropic's Claude model with no runtime overhead.", "body_md": "Developers waste minutes watching TypeScript compile before every Lambda deploy, even though the code never runs with types. By swapping ts-node for a combined esbuild + tsc --noEmit pipeline, you keep full type safety and slash build time. The result is a Lambda that talks to Claude with zero‑runtime overhead.\n\nWhen you write a Lambda in TypeScript you usually run **ts-node** (a tool that compiles on‑the‑fly) during local testing and then run **tsc** (the TypeScript compiler) as a separate step before packaging. Two things happen:\n\n`type‑only` imports stay in the bundle, adding bytes that the Lambda never uses.\nThink of the process like a chef who first tastes every ingredient, then cooks the whole dish again from scratch. The taste test is useful, but doing it twice eats time and resources.\n\n**In plain English** – the traditional flow makes the build slower without giving you any extra runtime benefit.\n\n``` js\n// src/handler.ts\nimport { APIGatewayProxyEvent, APIGatewayProxyResult } from \"aws-lambda\";\nimport { Anthropic } from \"@anthropic-ai/sdk\";\n\nexport const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {\n  const body = JSON.parse(event.body ?? \"{}\");\n  const diff = body.diff as string; // type‑only check, but stays in bundle\n\n  const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });\n  const response = await client.completions.create({\n    model: \"claude-3-sonnet-20240229\",\n    prompt: `Review this diff and suggest improvements:\\n${diff}`,\n    max_tokens: 512,\n  });\n\n  return {\n    statusCode: 200,\n    body: JSON.stringify({ comment: response.completion }),\n  };\n};\n```\n\n*What you would normally do*\n\n```\n# 1️⃣ Type‑check\nnpx tsc --noEmit\n\n# 2️⃣ Bundle (often with esbuild or webpack)\nnpx esbuild src/handler.ts --bundle --platform=node --target=node22.0 --outfile=dist/handler.js\n\n# 3️⃣ Zip and upload to Lambda\nzip -j lambda.zip dist/handler.js\naws lambda update-function-code --function-name ReviewLambda --zip-file fileb://lambda.zip\n```\n\nTwo separate commands, two passes, and the final `handler.js` still contains the `type‑only` import for `APIGatewayProxyEvent`. The compile step becomes a noticeable delay in CI/CD pipelines.\n\n**esbuild** is a fast bundler written in Go; it can also strip type‑only imports automatically. By running **tsc** with `--noEmit` first, we let the TypeScript compiler do what it does best—verify that every variable matches its declared shape—without producing any JavaScript files. Then we hand the same source files to esbuild, which creates a tiny, ready‑to‑run bundle.\n\n`tsc --noEmit` stops after the `import type { … }` statements, shrinking the bundle.\n**Key takeaway** – you keep the type safety you love while letting esbuild do the heavy lifting of creating the final artifact.\n\n`tsconfig.json`\n\n```\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"strict\": true,\n    \"noEmit\": true,               // <-- important: do not write .js files\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"include\": [\"src/**/*.ts\"]\n}\n{\n  \"scripts\": {\n    \"type-check\": \"tsc\",\n    \"bundle\": \"esbuild src/handler.ts --bundle --platform=node --target=node22.0 --outfile=dist/handler.js --experimental-strip-types\",\n    \"build\": \"npm run type-check && npm run bundle\"\n  }\n}\n```\n\nThe `--experimental-strip-types` flag tells esbuild to delete any leftover type annotations that might have survived the bundling step (more on that later).\n\n`npm run build`\n\n```\n// package.json (relevant part)\n{\n  \"name\": \"claude-lambda\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    // Verify types, then produce a tiny bundle\n    \"type-check\": \"tsc\",\n    \"bundle\": \"esbuild src/handler.ts \\\\\n      --bundle \\\\\n      --platform=node \\\\\n      --target=node22.0 \\\\\n      --outfile=dist/handler.js \\\\\n      --experimental-strip-types\",\n    \"build\": \"npm run type-check && npm run bundle\"\n  },\n  \"dependencies\": {\n    \"@anthropic-ai/sdk\": \"^1.2.0\",\n    \"@aws-sdk/client-lambda\": \"^3.600.0\"\n  },\n  \"devDependencies\": {\n    \"esbuild\": \"^0.21.0\",\n    \"typescript\": \"^5.4.5\"\n  }\n}\n```\n\n*Explanation* \n\n`type-check` runs the compiler without writing files.\n`bundle` calls esbuild with the `--experimental-strip-types` flag (more in the next section).\n`build` chains the two, guaranteeing that you never ship code that failed type‑checking.\nEven though we asked tsc not to emit JavaScript, some type‑only imports can slip into the final bundle if we’re not careful. For example, a statement like `import { type Request } from \"./types\"` is removed by esbuild, but a **value‑side import** that only contains types can be mistakenly kept if the code references it in a way the bundler thinks is a runtime use.\n\n`satisfies` operator as a safety net\nThe **satisfies** operator (added in TypeScript 4.9) lets you assert that a value matches a given type *without* changing the inferred type of the value. When you write:\n\n``` js\nconst payload = {\n  diff: event.body?.diff ?? \"\",\n} satisfies ReviewRequest;\n```\n\n`payload` has the shape expected by the Claude SDK.\n\n```\n// src/types.ts\nexport interface ReviewRequest {\n  /** The raw git diff that needs a review */\n  diff: string;\n}\n\n// src/handler.ts\nimport { APIGatewayProxyEvent, APIGatewayProxyResult } from \"aws-lambda\";\nimport { Anthropic } from \"@anthropic-ai/sdk\";\nimport type { ReviewRequest } from \"./types\"; // type‑only import, will be stripped\n\nexport const handler = async (\n  event: APIGatewayProxyEvent\n): Promise<APIGatewayProxyResult> => {\n  // Parse incoming JSON safely\n  const body = JSON.parse(event.body ?? \"{}\");\n\n  // Use `satisfies` to make sure the shape matches ReviewRequest\n  const request = {\n    diff: body.diff ?? \"\",\n  } satisfies ReviewRequest; // <-- compile‑time only, removed later\n\n  // -----------------------------------------------------------------\n  // The rest of the function talks to Claude – see next section\n  // -----------------------------------------------------------------\n  const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });\n\n  const response = await client.completions.create({\n    model: \"claude-3-sonnet-20240229\",\n    prompt: `Please review the following diff and suggest improvements:\\n${request.diff}`,\n    max_tokens: 512,\n  });\n\n  return {\n    statusCode: 200,\n    body: JSON.stringify({ comment: response.completion }),\n  };\n};\n```\n\n**Tip** – `satisfies` is perfect for validating request payloads that come from the outside world (API Gateway, SQS, etc.) because it does not affect the runtime value.\n\nIf you accidentally wrote:\n\n``` js\nimport { ReviewRequest } from \"./types\"; // not `type` import\n```\n\nesbuild would keep the import, increasing bundle size, and the code would try to `require` a file that only contains TypeScript interfaces, causing a runtime error in Lambda. Using `type` imports **or** the `satisfies` pattern prevents that silent breakage.\n\nNow that the build pipeline is fast and lean, let’s focus on the actual work: sending a diff to **Claude** (Anthropic’s LLM) and returning a comment.\n\nThe `@anthropic-ai/sdk` package ships with full TypeScript definitions. When you call `client.completions.create`, the compiler can verify that you provide every required field (`model`, `prompt`, `max_tokens`, …). That prevents a costly API error that would otherwise appear only after the Lambda runs.\n\n``` js\n// src/handler.ts\nimport { APIGatewayProxyEvent, APIGatewayProxyResult } from \"aws-lambda\";\nimport { Anthropic } from \"@anthropic-ai/sdk\";\nimport type { ReviewRequest } from \"./types\";\n\n/**\n * Lambda entry point.\n * Receives a JSON body `{ \"diff\": \"...git diff...\" }`,\n * asks Claude for a review, and returns `{ \"comment\": \"...\" }`.\n */\nexport const handler = async (\n  event: APIGatewayProxyEvent\n): Promise<APIGatewayProxyResult> => {\n  // 1️⃣ Guard against missing body\n  if (!event.body) {\n    return { statusCode: 400, body: JSON.stringify({ error: \"No body\" }) };\n  }\n\n  // 2️⃣ Parse and validate payload using `satisfies`\n  const raw = JSON.parse(event.body);\n  const payload = {\n    diff: raw.diff ?? \"\",\n  } satisfies ReviewRequest;\n\n  // 3️⃣ Prepare the Anthropic client – reads API key from environment\n  const anthropic = new Anthropic({\n    // The SDK expects a plain string; we assert its existence at runtime\n    apiKey: process.env.ANTHROPIC_API_KEY!,\n  });\n\n  // 4️⃣ Build the prompt – keep it short to stay within token limits\n  const prompt = `You are a code reviewer bot. Review this diff and suggest any improvements or fixes.\\n\\n${payload.diff}`;\n\n  // 5️⃣ Call Claude – type‑checked arguments\n  const completion = await anthropic.completions.create({\n    model: \"claude-3-sonnet-20240229\", // model name must be exact\n    max_tokens: 512,                   // limit response size for cost control\n    prompt,\n  });\n\n  // 6️⃣ Return the comment as JSON\n  return {\n    statusCode: 200,\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ comment: completion.completion }),\n  };\n};\n```\n\n| Line | Reason | \n|---|---|\n| `import { APIGatewayProxyEvent, APIGatewayProxyResult } from \"aws-lambda\"` | Types that describe the shape of the incoming request and outgoing response; they disappear after bundling. | \n| `import { Anthropic } from \"@anthropic-ai/sdk\"` | The real client that will make HTTP calls to Claude. | \n| `type ReviewRequest` import | Only used for compile‑time checks; removed by esbuild. | \n| `if (!event.body) …` | Defensive programming – Lambda should return a clear 400 when the caller forgets to send data. | \n| `payload satisfies ReviewRequest` | Guarantees the object matches the expected interface without emitting extra code. | \n| `new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! })` | Reads the secret from Lambda env vars; the `!` tells TypeScript we are sure it exists (otherwise it would be `string | \n| {% raw %} `prompt` construction | Simple string interpolation; you could add more context here if you like. | \n| `anthropic.completions.create` | Typed call – the compiler warns if a required field is missing. | \n| `return { … }` | Sends back JSON with the comment. The `Content-Type` header is required for API Gateway to treat it as JSON. | \n\n`require('esm')` in a layer silently fails. The fix is to avoid the layer or switch to an ES‑module‑compatible version of the SDK.\n**In plain English** – the code above stays within the safe zone of both SDKs: it uses only ES‑module imports, reads secrets from environment variables, and respects API limits.\n\nRunning a Lambda that talks to an external LLM can be expensive if you don’t know how often it’s invoked or how long Claude takes to respond. Adding heavy‑weight monitoring libraries defeats the purpose of a tiny bundle. Instead, we can use **Node’s built‑in `diagnostics_channel`** to emit lightweight events that CloudWatch can capture without adding code size.\n\n`diagnostics_channel`?\nA core module that lets you create a named channel and publish arbitrary data. Other parts of your system (or a CloudWatch subscription) can listen and log it. Because it’s built into Node, there is no extra dependency.\n\n``` js\n// src/metrics.ts\nimport { channel } from \"node:diagnostics_channel\";\n\n/**\n * A channel named \"claude-lambda\" that emits timing info.\n * Listeners can subscribe to this channel in CloudWatch Logs Insights.\n */\nexport const claudeChannel = channel(\"claude-lambda\");\n\n// Helper to measure async functions\nexport async function withTiming<T>(label: string, fn: () => Promise<T>): Promise<T> {\n  const start = Date.now();\n  try {\n    const result = await fn();\n    return result;\n  } finally {\n    const durationMs = Date.now() - start;\n    // Emit an object – listeners can filter by `label`\n    claudeChannel.publish({ label, durationMs });\n  }\n}\n```\n\nNow wrap the Claude call:\n\n``` js\nimport { withTiming } from \"./metrics\";\n\n// inside handler\nconst completion = await withTiming(\"anthropic-call\", async () => {\n  return anthropic.completions.create({\n    model: \"claude-3-sonnet-20240229\",\n    max_tokens: 512,\n    prompt,\n  });\n});\n```\n\n**Key takeaway** – you get millisecond‑level visibility without pulling in a big monitoring SDK, keeping the bundle under 100 KB.\n\n`\"claude-lambda\"` JSON.\n\n```\nfields @timestamp, @message\n| filter @message like /claude-lambda/\n| parse @message \"*label\\\":\\\"*\\\",*durationMs\\\":*}\" as label, duration\n| stats avg(duration) as avgMs, count() as calls by label\n```\n\nYou’ll see average latency per label, letting you spot spikes in Claude response time.\n\n**You now have a repeatable pattern for building ultra‑fast, type‑safe Lambdas that call Claude.** \n\nBy following these steps you cut build minutes, shrink Lambda zip size, and keep the safety net of TypeScript—all while getting valuable code‑review suggestions from Claude in real time. Happy coding!\n\n**Transparency notice**\n\nThis article was written with the help of an AI system — [Groq](https://groq.com) (GPT OSS 120B).\n\n**Published:** 2026-09-16 · **Primary focus:** TypeScriptBuild\n\nAll code blocks are intended to be correct and runnable, but please verify them\n\nagainst [the TypeScript docs](https://www.typescriptlang.org/docs) before using in production.\n\n*Find an error? Drop a comment — corrections are always welcome.*", "url": "https://wpnews.pro/news/how-to-build-a-lightning-fast-typescript-lambda-that-calls-claude-using-esbuild", "canonical_source": "https://dev.to/dineshgowtham/how-to-build-a-lightning-fast-typescript-lambda-that-calls-claude-using-esbuild-and-tsc-noemit-dnn", "published_at": "2026-09-16 15:32:07+00:00", "updated_at": "2026-09-16 15:43:27.480391+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["TypeScript", "esbuild", "tsc", "AWS Lambda", "Anthropic", "Claude"], "alternates": {"html": "https://wpnews.pro/news/how-to-build-a-lightning-fast-typescript-lambda-that-calls-claude-using-esbuild", "markdown": "https://wpnews.pro/news/how-to-build-a-lightning-fast-typescript-lambda-that-calls-claude-using-esbuild.md", "text": "https://wpnews.pro/news/how-to-build-a-lightning-fast-typescript-lambda-that-calls-claude-using-esbuild.txt", "jsonld": "https://wpnews.pro/news/how-to-build-a-lightning-fast-typescript-lambda-that-calls-claude-using-esbuild.jsonld"}}