or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cache-persistence.mdconfiguration.mddecorator.mddynamodb-persistence.mderrors.mdfunction-wrapper.mdindex.mdmiddleware.mdtypes.md
tile.json

middleware.mddocs/

Middy Middleware

The makeHandlerIdempotent function creates Middy middleware that makes Lambda handlers idempotent. It integrates seamlessly with the Middy middleware framework.

Capabilities

Make Handler Idempotent Middleware

Creates Middy middleware to make Lambda handlers idempotent by tracking executions in a persistence store.

/**
 * Creates Middy middleware to make Lambda handlers idempotent.
 *
 * The middleware runs before the handler executes to check for existing records,
 * after the handler completes to save the result, and on errors to clean up.
 *
 * @param options - Configuration options for idempotency behavior
 * @returns Middy middleware object with before, after, and onError hooks
 */
function makeHandlerIdempotent(
  options: IdempotencyLambdaHandlerOptions
): MiddlewareLikeObj;

interface IdempotencyLambdaHandlerOptions {
  /** Persistence layer to store idempotency records */
  persistenceStore: BasePersistenceLayer;
  /** Optional configuration for idempotency behavior */
  config?: IdempotencyConfig;
  /** Optional custom prefix for idempotency keys */
  keyPrefix?: string;
}

interface MiddlewareLikeObj {
  /** Runs before the handler executes */
  before: (request: MiddyLikeRequest) => unknown;
  /** Runs after successful handler execution */
  after: (request: MiddyLikeRequest) => Promise<void>;
  /** Runs when an error occurs in the handler */
  onError: (request: MiddyLikeRequest) => Promise<void>;
}

Usage Examples:

Basic Middy Middleware Usage

import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import middy from '@middy/core';
import type { Context, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const lambdaHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<APIGatewayProxyResult> => {
  // Your business logic
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'Success' }),
  };
};

export const handler = middy(lambdaHandler).use(
  makeHandlerIdempotent({ persistenceStore })
);

With Configuration Options

import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import { IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import middy from '@middy/core';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const config = new IdempotencyConfig({
  hashFunction: 'md5',
  useLocalCache: true,
  expiresAfterSeconds: 3600,
  throwOnNoIdempotencyKey: false,
  eventKeyJmesPath: 'body',
});

export const handler = middy(
  async (event: APIGatewayProxyEvent, context: Context): Promise<void> => {
    // Your logic
  }
).use(makeHandlerIdempotent({ config, persistenceStore }));

Using JMESPath for Idempotency Key

import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import { IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import middy from '@middy/core';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const config = new IdempotencyConfig({
  // Use a specific header as the idempotency key
  eventKeyJmesPath: 'headers."idempotency-key"',
});

export const handler = middy(
  async (event: APIGatewayProxyEvent, context: Context): Promise<void> => {
    // Your logic
  }
).use(makeHandlerIdempotent({ config, persistenceStore }));

With Multiple Middleware

import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import middy from '@middy/core';
import httpErrorHandler from '@middy/http-error-handler';
import httpJsonBodyParser from '@middy/http-json-body-parser';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

const lambdaHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<void> => {
  // Your logic - body is already parsed by httpJsonBodyParser
  console.log(event.body);
};

export const handler = middy(lambdaHandler)
  .use(httpJsonBodyParser())
  .use(makeHandlerIdempotent({ persistenceStore }))
  .use(httpErrorHandler());

With Custom Key Prefix

import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import middy from '@middy/core';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
  tableName: 'idempotencyTable',
});

export const handler = middy(
  async (event: APIGatewayProxyEvent, context: Context): Promise<void> => {
    // Your logic
  }
).use(
  makeHandlerIdempotent({
    persistenceStore,
    keyPrefix: 'payment-service',
  })
);

With Cache Persistence

import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
import { createClient } from '@redis/client';
import middy from '@middy/core';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

// Create and connect Redis client
const redisClient = await createClient({
  url: `rediss://${process.env.REDIS_ENDPOINT}:${process.env.REDIS_PORT}`,
}).connect();

const persistenceStore = new CachePersistenceLayer({
  client: redisClient,
});

const config = new IdempotencyConfig({
  expiresAfterSeconds: 300, // 5 minutes
  useLocalCache: true,
});

export const handler = middy(
  async (event: APIGatewayProxyEvent, context: Context): Promise<void> => {
    // Your logic
  }
).use(makeHandlerIdempotent({ persistenceStore, config }));

Behavior

The middleware operates in three phases:

  1. Before Hook:

    • Extracts the event payload
    • Checks for existing idempotency records
    • If duplicate found, returns cached response (early return)
    • If new request, saves "in progress" record and continues
  2. After Hook:

    • Saves the successful response to the persistence store
    • Updates the record status to "COMPLETED"
  3. OnError Hook:

    • Deletes the "in progress" record if an error occurs
    • Allows the request to be retried

Important Notes

  • Return Value Required: For the middleware to work properly, your Lambda handler must return a value different from undefined. This is a known limitation of the early return feature in Middy.js
  • Early Returns: If your use case requires early returns with undefined, use the makeIdempotent() function wrapper instead
  • Middleware Order: Place makeHandlerIdempotent after input parsing middleware (like httpJsonBodyParser) but before error handling middleware
  • Full Event Hashing: By default, the entire event object is used for the idempotency key. Use eventKeyJmesPath to use only part of the event

Error Handling

The middleware may throw various idempotency errors:

  • IdempotencyKeyError - When no idempotency key can be extracted
  • IdempotencyItemAlreadyExistsError - When a duplicate request is detected
  • IdempotencyAlreadyInProgressError - When a request is already being processed
  • IdempotencyValidationError - When payload validation fails

When errors occur, the middleware's onError hook cleans up the in-progress record to allow retries.