Middleware examples

Real-world examples using the v4 class-based middleware API. See Lifecycle for the full hook reference.

Dependency injection

Inject a Prisma client into all functions via transformFunctionInput. Types are automatically inferred, so your functions see a typed prisma property.

import { Middleware } from "inngest";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

class PrismaMiddleware extends Middleware.BaseMiddleware {
  transformFunctionInput(args: Middleware.TransformFunctionInputArgs) {
    return {
      ...args,
      ctx: {
        ...args.ctx,
        prisma,
      },
    };
  }
}

const inngest = new Inngest({
  id: "my-app",
  middleware: [PrismaMiddleware],
});
// prisma is typed and available in every function
inngest.createFunction(
  { id: "create-audit-log", triggers: { event: "app/user.loggedin" } },
  async ({ prisma, event }) => {
    await prisma.auditTrail.create({
      data: { userId: event.data.userId, action: "login" },
    });
  }
);

Request headers to context

Use wrapRequest to capture incoming HTTP headers, then transformFunctionInput to expose them in function context.

import { Middleware } from "inngest";

class HeadersMiddleware extends Middleware.BaseMiddleware {
  private headers: Record<string, string> = {};

  async wrapRequest({ next, requestInfo }: Middleware.WrapRequestArgs) {
    this.headers = { ...requestInfo.headers };
    return await next();
  }

  transformFunctionInput(args: Middleware.TransformFunctionInputArgs) {
    return {
      ...args,
      ctx: {
        ...args.ctx,
        headers: this.headers,
      },
    };
  }
}

Observability

Log function and step lifecycle events for monitoring and metrics.

import { Middleware } from "inngest";

class ObservabilityMiddleware extends Middleware.BaseMiddleware {
  onRunStart({ functionInfo }: Middleware.OnRunStartArgs) {
    console.log(`[run:start] ${functionInfo.id}`);
  }

  onRunComplete({ functionInfo, output }: Middleware.OnRunCompleteArgs) {
    console.log(`[run:complete] ${functionInfo.id}`, output);
  }

  onRunError({ functionInfo, error, isFinalAttempt }: Middleware.OnRunErrorArgs) {
    console.error(`[run:error] ${functionInfo.id}`, error);
    if (isFinalAttempt) {
      // Send to error tracking service
    }
  }

  onStepComplete({ functionInfo, stepInfo }: Middleware.OnStepCompleteArgs) {
    console.log(`[step:complete] ${functionInfo.id} > ${stepInfo.hashedId}`);
  }

  onStepError({ functionInfo, stepInfo, error }: Middleware.OnStepErrorArgs) {
    console.error(`[step:error] ${functionInfo.id} > ${stepInfo.hashedId}`, error);
  }
}

Error handling

Use wrapStepHandler to catch step errors and convert them to NonRetriableError.

import { Middleware, NonRetriableError } from "inngest";

class ErrorHandlingMiddleware extends Middleware.BaseMiddleware {
  async wrapStepHandler({ next }: Middleware.WrapStepHandlerArgs) {
    try {
      return await next();
    } catch (err) {
      // Convert specific errors to non-retriable
      if (err instanceof ValidationError) {
        throw new NonRetriableError(err.message, { cause: err });
      }
      throw err;
    }
  }
}

wrapStepHandler is used here instead of wrapStep because it runs on every errored attempt, while wrapStep only runs after the step has exhausted all retries.

Custom serialization

Inngest serializes and deserializes data as JSON. This means that non-JSON types like Date, Map, or Set are lost. However, you can preserve them using serializer middleware.

Serializer middleware creates JSON-valid representations of non-JSON types, allowing for seamless preservation across step boundaries, event sends, and function invocations. See the Custom serialization guide for a full walkthrough.

Inserting steps

Use wrapFunctionHandler to run steps before or after the function handler. Because wrapping hooks have access to ctx, you can call step.run() directly.

import { Middleware } from "inngest";

class InsertStepsMiddleware extends Middleware.BaseMiddleware {
  async wrapFunctionHandler({ ctx, next }: Middleware.WrapFunctionHandlerArgs) {
    // Run a step before the function handler
    await ctx.step.run("setup", async () => {
      // e.g. initialize resources
    });

    const functionOutput = await next();

    // Run a step after the function handler
    await ctx.step.run("cleanup", async () => {
      // e.g. send a notification, update a status record
    });

    return functionOutput;
  }
}

The same pattern works with wrapStep to insert steps around individual step executions.

import { Middleware } from "inngest";

class StepAuditMiddleware extends Middleware.BaseMiddleware {
  async wrapStep({ ctx, next, stepInfo }: Middleware.WrapStepArgs) {
    try {
      return await next();
    } catch (err) {
      // Record the failure in a durable step
      await ctx.step.run(`audit-failure-${stepInfo.hashedId}`, async () => {
        await auditLog.write({ step: stepInfo.hashedId, error: err });
      });

      throw err;
    }
  }
}

Steps inserted by a middleware's wrapStep will not trigger that same middleware's wrapStep again. The SDK prevents this to avoid infinite loops.