npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@composurecdk/lambda

v0.11.0

Published

Composable Lambda function builder with well-architected defaults

Downloads

1,171

Readme

@composurecdk/lambda

Lambda builders for ComposureCDK.

This package provides a fluent builder for AWS Lambda functions with secure, AWS-recommended defaults. It wraps the CDK Function construct — refer to the CDK documentation for the full set of configurable properties.

Function Builder

import { createFunctionBuilder } from "@composurecdk/lambda";

const handler = createFunctionBuilder()
  .runtime(Runtime.NODEJS_22_X)
  .handler("index.handler")
  .code(Code.fromAsset("lambda"))
  .build(stack, "MyFunction");

Every FunctionProps property is available as a fluent setter on the builder.

Secure Defaults

createFunctionBuilder applies the following defaults. Each can be overridden via the builder's fluent API.

| Property | Default | Rationale | | --------------- | -------- | ------------------------------------------------------------------------------------ | | tracing | ACTIVE | Enables X-Ray distributed tracing for observability. | | loggingFormat | JSON | Structured logs for CloudWatch Logs Insights auto-discovery and consistent querying. |

These defaults are guided by the AWS Well-Architected Serverless Applications Lens.

The defaults are exported as FUNCTION_DEFAULTS for visibility and testing:

import { FUNCTION_DEFAULTS } from "@composurecdk/lambda";

Overriding defaults

import { LoggingFormat, Tracing } from "aws-cdk-lib/aws-lambda";

const handler = createFunctionBuilder()
  .runtime(Runtime.NODEJS_22_X)
  .handler("index.handler")
  .code(Code.fromAsset("lambda"))
  .tracing(Tracing.PASS_THROUGH)
  .loggingFormat(LoggingFormat.TEXT)
  .build(stack, "MyFunction");

Environment variables

Each value in .environment(...) is independently Resolvable, so a variable can carry a sibling's output while the rest stay literal:

import { compose, ref } from "@composurecdk/core";
import { createRestApiBuilder, type RestApiBuilderResult } from "@composurecdk/apigateway";

compose(
  {
    api: createRestApiBuilder().restApiName("Orders"),
    worker: createFunctionBuilder()
      .runtime(Runtime.NODEJS_22_X)
      .handler("index.handler")
      .code(Code.fromAsset("lambda"))
      .environment({
        API_URL: ref("api", (r: RestApiBuilderResult) => r.api.url),
        LOG_LEVEL: "info",
      }),
  },
  { api: [], worker: ["api"] },
);

Per value rather than over the whole record, because mixing the two is the common case: wrapping the record would push every literal through the same ref, and through combine as soon as two siblings are involved.

Environment variable encryption

Lambda encrypts environment variables at rest with an AWS-managed key. .environmentEncryption(...) opts into a customer-managed one, and accepts a concrete key or a Resolvable, so a key built by @composurecdk/kms can be a component of the same system rather than a construct created before compose:

import { compose, ref } from "@composurecdk/core";
import { createKeyBuilder, type KeyBuilderResult } from "@composurecdk/kms";

compose(
  {
    envKey: createKeyBuilder().description("Encrypts the checkout handler's environment."),
    checkout: createFunctionBuilder()
      .runtime(Runtime.NODEJS_22_X)
      .handler("index.handler")
      .code(Code.fromAsset("lambda"))
      .environmentEncryption(ref<KeyBuilderResult>("envKey").get("key")),
  },
  { envKey: [], checkout: ["envKey"] },
);

Execution role

By default, createFunctionBuilder creates an explicit IAM execution role with an inline LogsWriter policy scoped to the function's auto-created log group:

  • logs:CreateLogStream and logs:PutLogEvents on the function's specific log group ARN.
  • No logs:CreateLogGroup (the builder pre-creates the group).
  • No AWSLambdaBasicExecutionRole managed policy — that policy grants the same actions on *, allowing a compromised function to write to any log group in the account.

Supplying your own logGroup keeps that scoping: the ARN is read from wherever the aws-cdk-lib you have installed keeps it, so a log group in either the L2 or the reference form works. One that exposes no ARN at all fails the build by name rather than emitting an unscoped or malformed policy — reach for .role() or .useCdkAutoRole() if you need the builder to stay out of the role entirely.

The role is exposed on the build result:

const result = createFunctionBuilder()
  .runtime(Runtime.NODEJS_22_X)
  .handler("index.handler")
  .code(Code.fromAsset("lambda"))
  .build(stack, "MyFunction");

result.role; // IRole — the execution role attached to the function

CDK continues to layer X-Ray, VPC, KMS-env, DLQ, and EFS permissions onto the role automatically based on the function's other props.

Extending the default role: .configureRole(fn)

For least-privilege grants alongside the defaults:

import { createStatementBuilder } from "@composurecdk/iam";

const handler = createFunctionBuilder()
  .runtime(Runtime.NODEJS_22_X)
  .handler("index.handler")
  .code(Code.fromAsset("lambda"))
  .configureRole((role) =>
    role.addInlinePolicyStatements("OrdersRead", [
      createStatementBuilder()
        .allow()
        .actions(["dynamodb:GetItem", "dynamodb:Query"])
        .resources([table.tableArn]),
    ]),
  );

The callback receives the internal IRoleBuilder. Calling configureRole more than once replaces the previous callback. The reserved LogsWriter name throws at build time if added a second time.

Supplying a role: .role(role)

For a fully external role. The builder skips creating its own role and does not auto-attach the LogsWriter policy — the caller takes responsibility for permissions. Accepts a concrete IRole or a ref(...) for cross-component wiring under compose:

import { compose, ref } from "@composurecdk/core";
import { createServiceRoleBuilder, type RoleBuilderResult } from "@composurecdk/iam";

compose(
  {
    sharedRole: createServiceRoleBuilder("lambda.amazonaws.com"),
    handler: createFunctionBuilder()
      .runtime(Runtime.NODEJS_22_X)
      .handler("index.handler")
      .code(Code.fromAsset("lambda"))
      .role(ref("sharedRole", (r: RoleBuilderResult) => r.role)),
  },
  { sharedRole: [], handler: ["sharedRole"] },
).build(stack, "MySystem");

Escape hatch: .useCdkAutoRole()

Opt back into CDK's auto-created role attached to AWSLambdaBasicExecutionRole. Not the recommended path — it re-introduces the wildcard log surface — but available for matching an existing stack's logical IDs during a phased migration.

.role(), .configureRole(), and .useCdkAutoRole() are mutually exclusive; combining any two throws at build time.

Recommended Alarms

The builder creates AWS-recommended CloudWatch alarms by default. No alarm actions are configured — access alarms from the build result to add SNS topics or other actions.

| Alarm | Metric | Default threshold | Created when | | ------------------------ | --------------------------------- | --------------------------- | -------------------------------------- | | errors | Errors (Sum, 1 min) | > 0 | Always | | throttles | Throttles (Sum, 1 min) | > 0 | Always | | duration | Duration (p99, 1 min) | > 90% of configured timeout | timeout is set | | concurrentExecutions | ConcurrentExecutions (Max, 1 min) | >= 80% of reserved limit | reservedConcurrentExecutions is set | | <key>FailedInvocations | FailedInvokeEventCount (Sum) | > 0 | An SQS or DynamoDB source is attached | | <key>DroppedEvents | DroppedEventCount (Sum) | > 0 | An SQS or DynamoDB source is attached | | iteratorAge | IteratorAge (Max, 1 min) | > 60000 ms for 3 min¹ | A stream source (DynamoDB) is attached |

¹ AWS recommends alarming on IteratorAge for stream consumers but prescribes no fixed threshold — it is workload dependent. The 60s/3-minute default is deliberately conservative; tune it per workload via eventSourceIteratorAge.

The per-mapping event-source alarms are contextual: one pair is created per event source attached via addEventSource (see Event sources) whose kind emits per-mapping ESM metrics. Each alarm's key is the event source's key suffixed with FailedInvocations / DroppedEvents — e.g. an event source added as "orders" produces ordersFailedInvocations and ordersDroppedEvents. The eventSourceFailedInvocations / eventSourceDroppedEvents fields on recommendedAlarms tune every such alarm.

iteratorAge is different: IteratorAge is a function-level metric, so a single alarm (keyed iteratorAge) is created whenever at least one stream source (currently DynamoDB streams) is attached, regardless of how many. It warns when the consumer falls behind its stream. Tune or disable it via the eventSourceIteratorAge field on recommendedAlarms.

The defaults are exported as FUNCTION_ALARM_DEFAULTS for visibility and testing:

import { FUNCTION_ALARM_DEFAULTS } from "@composurecdk/lambda";

The duration and concurrentExecutions alarms use percentage-based thresholds that automatically adjust when the base value changes. For example, if you change the function timeout from 30s to 60s, the duration alarm threshold adjusts from 27s to 54s without any configuration change.

Customizing thresholds

Override individual alarm properties via recommendedAlarms. Unspecified fields keep their defaults.

Absolute-threshold alarms (errors, throttles) accept a threshold value:

const handler = createFunctionBuilder()
  .runtime(Runtime.NODEJS_22_X)
  .handler("index.handler")
  .code(Code.fromAsset("lambda"))
  .recommendedAlarms({
    errors: { threshold: 5, evaluationPeriods: 3, datapointsToAlarm: 2 },
  });

Percentage-based alarms (duration, concurrentExecutions) accept a thresholdPercent between 0 and 1:

builder.timeout(Duration.seconds(30)).recommendedAlarms({
  duration: { thresholdPercent: 0.75 }, // 75% of timeout = 22.5s
});

For a fixed absolute threshold, disable the recommended alarm and add a custom one via addAlarm.

Disabling alarms

Disable all recommended alarms:

builder.recommendedAlarms(false);
// or
builder.recommendedAlarms({ enabled: false });

Disable individual alarms:

builder.recommendedAlarms({ errors: false, throttles: false });

Custom alarms

Add custom alarms alongside the recommended ones via addAlarm. The callback receives an AlarmDefinitionBuilder typed to the Lambda function, so the metric factory has access to the function's built-in metric helpers.

const handler = createFunctionBuilder()
  .runtime(Runtime.NODEJS_22_X)
  .handler("index.handler")
  .code(Code.fromAsset("lambda"))
  .timeout(Duration.seconds(30))
  .addAlarm("highInvocations", (alarm) =>
    alarm
      .metric((fn) => fn.metricInvocations({ period: Duration.minutes(1) }))
      .threshold(1000)
      .greaterThanOrEqual()
      .description("Invocation count is unusually high"),
  );

Custom alarm keys must not conflict with recommended alarm keys. To replace a recommended alarm, disable it first and add a custom one with the same key.

Applying alarm actions

Alarms are returned in the build result as Record<string, Alarm>:

const result = handler.build(stack, "MyFunction");

const alertTopic = new Topic(stack, "AlertTopic");
for (const alarm of Object.values(result.alarms)) {
  alarm.addAlarmAction(new SnsAction(alertTopic));
}

Event sources

addEventSource(key, source) wires a queue or stream to the function. A Lambda function can have many event sources of mixed types, so the hook is repeatable and keyed — the resolved sources are exposed on result.eventSources.

Pass a ComposureEventSource from a factory (sqsEventSource, dynamoEventSource), which carries its own Resolvable so the source queue or table can be a ref() to a sibling component, or a bare CDK IEventSource as an escape hatch.

import { compose, ref } from "@composurecdk/core";
import { createFunctionBuilder, sqsEventSource } from "@composurecdk/lambda";
import { createQueueBuilder } from "@composurecdk/sqs";

const system = compose(
  {
    orders: createQueueBuilder().queueName("orders"),
    processor: createFunctionBuilder()
      .runtime(Runtime.NODEJS_22_X)
      .handler("index.handler")
      .code(Code.fromAsset("lambda"))
      .addEventSource("orders", sqsEventSource(ref("orders", (r) => r.queue))),
  },
  { orders: [], processor: ["orders"] },
);

The source is attached after the function and its least-privilege execution role exist, so the source.bind(fn) that addEventSource performs grants the consume permission (SQS ReceiveMessage, or DynamoDB grantStreamRead) onto the builder's role rather than CDK's auto-role.

dynamoEventSource(table, props?) mirrors the SQS factory for DynamoDB streams. The table must have a stream enabled (via the DynamoDB builder's .dynamoStream(...) / .stream(...), or TableProps.stream); otherwise CDK throws DynamoDB Streams must be enabled at build time. startingPosition defaults to LATEST and is overridable via props:

import { StartingPosition } from "aws-cdk-lib/aws-lambda";
import { StreamViewType } from "aws-cdk-lib/aws-dynamodb";
import { compose, ref } from "@composurecdk/core";
import { createFunctionBuilder, dynamoEventSource } from "@composurecdk/lambda";
import { createTableV2Builder } from "@composurecdk/dynamodb";

compose(
  {
    orders: createTableV2Builder()
      .partitionKey({ name: "pk", type: AttributeType.STRING })
      .dynamoStream(StreamViewType.NEW_AND_OLD_IMAGES),
    processor: createFunctionBuilder()
      .runtime(Runtime.NODEJS_22_X)
      .handler("index.handler")
      .code(Code.fromAsset("lambda"))
      .addEventSource(
        "orders",
        dynamoEventSource(
          ref("orders", (r) => r.table),
          {
            startingPosition: StartingPosition.TRIM_HORIZON,
          },
        ),
      ),
  },
  { orders: [], processor: ["orders"] },
);

Secure defaults

sqsEventSource applies AWS-recommended defaults, each overridable via the second props argument and exported as DEFAULT_SQS_EVENT_SOURCE_PROPS:

| Property | Default | Rationale | | ------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------- | | reportBatchItemFailures | true | A single poison message fails only its own record, not the whole batch. CDK defaults this false. | | metricsConfig | { metrics: [EventCount] } | Enables the per-mapping ESM metrics that back the event-source contextual alarms. |

dynamoEventSource applies the same defaults plus startingPosition and bisectBatchOnError, exported as DEFAULT_DYNAMO_EVENT_SOURCE_PROPS (the shared DEFAULT_STREAM_EVENT_SOURCE_PROPS, which a future kinesisEventSource reuses):

| Property | Default | Rationale | | ------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | startingPosition | LATEST | A newly-attached consumer reads from the stream tip, not the source's existing change history. | | reportBatchItemFailures | true | A single poison record fails only its own record, not the whole batch. CDK defaults this false. | | bisectBatchOnError | true | Split and retry a failing batch, isolating a poison record instead of blocking the shard. CDK defaults this false. | | metricsConfig | { metrics: [EventCount] } | Enables the per-mapping ESM metrics that back the event-source contextual alarms. |

startingPosition is optional on dynamoEventSource (the default supplies it) — unlike CDK's DynamoEventSourceProps, which requires it.

Failure handling (dead-letter queue)

Bisecting isolates a poison record, but a record that never succeeds is still retried until the stream's 24 h retention window expires unless you bound retryAttempts / maxRecordAge. Bounding retries without a dead-letter destination drops those records silently — so for durable failure handling, set onFailure too. dynamoEventSource widens onFailure to accept a queue (or a ref() to a sibling createQueueBuilder("dlq") result) and wraps it in an SqsDlq for you, resolving it alongside the table ref via combine:

.addEventSource(
  "orders",
  dynamoEventSource(ref("orders", (r) => r.table), {
    retryAttempts: 3,
    onFailure: ref("dlq", (r) => r.queue),
  }),
)

A queue is the common case, not the only one: onFailure accepts any IEventSourceDlq — concrete or behind a ref() — and passes it through unwrapped. An S3 failure destination, which captures the whole failed batch rather than just the record metadata a DLQ message carries, wires up the same way:

.addEventSource(
  "orders",
  dynamoEventSource(ref("orders", (r) => r.table), {
    retryAttempts: 3,
    onFailure: ref("failures", (r) => new S3OnFailureDestination(r.bucket)),
  }),
)

An S3 destination on a DynamoDB stream mapping needs aws-cdk-lib ≥ 2.184.0, above this package's floor of 2.168.0. DynamoEventSource only opts into S3 destinations from that release; below it CDK rejects the pairing at synth with S3 onFailure Destination is not supported for this event source. The limit is CDK's, not this package's — a queue destination works at the floor.

If retries or record-age are bounded but no onFailure destination is set, a suppressible synth-time warning (STREAM_DLQ_WARNING_ID) fires. Silence it — when dropping is intended — with Annotations.of(scope).acknowledgeWarning(STREAM_DLQ_WARNING_ID).

Cross-component invariants

AWS Well-Architected guidance spans the queue and the function — the source queue's visibility timeout should be ≥ 6× the function timeout, and its redrive maxReceiveCount should be ≥ 5 before the DLQ. These are not enforced today (the queue often arrives as an unresolved ref()); they are tracked in #123 and #124.

kinesisEventSource is still deferred — see #120.

Deploy-time invocation: .invokeOnDeploy()

Some work has to happen as part of shipping the stack, and its outcome has to gate the deployment: registering the release with an external service, calling the system you just deployed to prove it answers, seeding reference data through a service API. .invokeOnDeploy() invokes the function once during deployment and fails the deployment if the function fails — the stack rolls back and the handler's error message lands in the CloudFormation events.

import { compose, ref } from "@composurecdk/core";
import { createFunctionBuilder } from "@composurecdk/lambda";

compose(
  {
    register: createFunctionBuilder()
      .runtime(Runtime.NODEJS_22_X)
      .handler("index.handler")
      .code(Code.fromAsset("register"))
      .timeout(Duration.seconds(30))
      .environment({ API_URL: "https://releases.example.com/v1/register" })
      .invokeOnDeploy({ after: [ref("api", (r: RestApiBuilderResult) => r.api)] }),

    api: createRestApiBuilder().restApiName("Orders"),
  },
  { api: [], register: ["api"] },
).build(stack, "Release");

What it is

If you already write CDK custom resources, the whole mental model is one sentence: it is CDK's triggers.Trigger, with the synchronous invocation fixed and the wait and ordering derived from the function. Everything below is detail on those defaults.

Worth stating plainly, because the other two tools look right and are not. custom_resources.Provider wants a handler of yours that speaks the CloudFormation custom-resource protocol. AwsCustomResource runs one fixed SDK call with no handler of yours — and reaching for Lambda.invoke there reports success when your handler fails. Trigger is the one that invokes a plain handler and fails the stack on an error, and it is the least known of the three.

It is a domain action on the function builder (ADR-0016): you declare the intent, the builder owns the invocation semantics, the wait derivation, the execution-role ordering and the IAM. The custom resource is exposed on the build result as deploymentTrigger, so a sibling can be ordered against the call itself (deploymentTrigger.executeBefore(...)).

Three things reach the template:

  • A Custom::Trigger, carrying the handler ARN, the invocation type, the wait, and ExecuteOnHandlerChange.
  • A trigger provider Lambda and its role — one per stack, shared by every Trigger in it, created by CDK rather than by this builder.
  • An AWS::Lambda::Version of your function. Trigger addresses the handler by its currentVersion, so calling .invokeOnDeploy() publishes a version — unconditionally, including under executeOnHandlerChange: false. That is the re-invocation mechanism rather than an accident: the version ARN changes when the function's code or configuration does, and that property change is what CloudFormation acts on.

The handler must throw

A handler that returns an error rather than throwing does not fail the deployment. Lambda considers an invocation that returns successful no matter what the payload says, so an HTTP-shaped { statusCode: 500 } — or a caught error folded into the response — deploys green:

export const handler = async () => {
  const response = await fetch(process.env.API_URL, { method: "POST" });
  if (!response.ok) {
    // Throw. Returning `{ statusCode: response.status }` here would deploy green.
    throw new Error(`Registration failed: ${response.status} ${await response.text()}`);
  }
};

Ordering

The function's own execution role is always waited for, so the policies attached to it by .grant() and .configureRole() exist before the handler runs — CloudFormation does not otherwise sequence those ahead of a custom resource, and the handler would race them.

That covers the identity half of a grant, which is not always the whole grant. grantDecrypt on a KMS key, or a cross-account bucket grant, also writes a resource policy, and that half lives in the granting resource's own tree where this ordering does not reach. If the handler needs it at deploy time, name that resource in after.

Everything else the call depends on goes in after as well. It takes any DependencySource — the same shapes as dependsOn in @composurecdk/custom-resources.

One caveat on "always": a role supplied with .role(Role.fromRoleArn(...)) contributes no resources to the template, so there is nothing to sequence and the wait is a no-op. Nothing is lost — an imported role's policies were never this stack's to create — but do not read it as a guarantee that they are in place.

Defaults

| Option | Default | Rationale | | ------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | invocation type | RequestResponse (not settable) | The deployment waits for the result. An asynchronous invocation returns before the work happens, reporting success whatever the handler did — the opposite of the point. | | timeout | function timeout + 30s | The deployment outlives the handler, so a handler that runs to its limit reports its error ("Task timed out after …") instead of the deployment abandoning the call. Capped at 14m30s. | | executeOnHandlerChange | true | Re-invokes when the handler's code or configuration changes; a no-op on unrelated stack updates. |

The derivation values are exported as INVOKE_ON_DEPLOY_DEFAULTS. With no function timeout set (or a token one), the wait falls back to 2 minutes, matching CDK's own Trigger default.

Whenever the wait ends up at or below the function's own timeout, a suppressible warning fires under INVOKE_ON_DEPLOY_TIMEOUT_WARNING_ID — the deployment would stop waiting while the handler is still running, so a slow call fails the stack as an abandoned invocation rather than reporting the handler's error. That covers an explicit timeout set too low, and also a function timeout at or above the 14m30s cap, where the cap leaves no margin to add.

The cap is not Lambda's 15-minute ceiling but CDK's trigger provider: that provider runs with a 15-minute Lambda timeout of its own and Trigger sets no ServiceTimeout on the custom resource, so a provider that dies mid-wait posts nothing back and CloudFormation is never told. The result is a hung deployment, not a failed one. .invokeOnDeploy() therefore throws on an explicit timeout above the cap rather than clamping it, and keeps 30s in hand on the derived path so the provider outlives the call it is waiting on.

What it does not do

  • It does not invoke on every deployment. With executeOnHandlerChange: true the invocation re-runs when the handler changes; with false it runs only on the stack's first deployment. CDK's Trigger offers no "always" mode.
  • It does not invoke on stack deletion. For teardown-time work, use @composurecdk/custom-resources, which has create/update/delete semantics.
  • It creates no alarms. The custom resource runs once per deployment, not continuously; there is no steady-state signal to alarm on. Failures surface as a failed deployment.
  • It is not a general SDK-call escape hatch. A call that has no CloudFormation resource but needs no handler of yours belongs in createAwsCustomResourceBuilder() — that builder invokes an AWS API directly, no Lambda of yours involved.

Examples

  • DualFunctionStack — Two Lambda functions with recommended alarms, custom alarms, and SNS alarm actions
  • MultiStackApp — Lambda split across stacks via .withStacks(), wired with ref
  • OrderProcessorStack — SQS queue wired to a Lambda consumer via sqsEventSource
  • DynamoStreamProcessorStack — DynamoDB stream wired to a Lambda consumer via dynamoEventSource, with bisect-on-error, bounded retries, and an onFailure SQS DLQ