# Middleware examples

Real-world examples using the v4 class-based middleware API. See [Lifecycle](/docs-markdown/reference/typescript/v4/middleware/lifecycle) for the full hook reference.

## Dependency injection

Inject a [Prisma](https://www.prisma.io/) client into all functions via `transformFunctionInput`. Types are automatically inferred, so your functions see a typed `prisma` property.

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

const prisma = new PrismaClient();

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

const inngest = new Inngest({
  id: "my-app",
  middleware: [PrismaMiddleware],
});
```

```ts
// 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.

```ts
import { Middleware } from "inngest";

class HeadersMiddleware extends Middleware.BaseMiddleware {
  id = "headers";
  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.

```ts
import { Middleware } from "inngest";

class ObservabilityMiddleware extends Middleware.BaseMiddleware {
  id = "o11y";
  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`.

```ts
import { Middleware, NonRetriableError } from "inngest";

class ErrorHandlingMiddleware extends Middleware.BaseMiddleware {
  id = "error-handling";
  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](/docs-markdown/reference/typescript/v4/middleware/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.

```ts
import { Middleware } from "inngest";

class InsertStepsMiddleware extends Middleware.BaseMiddleware {
  id = "insert-steps";
  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.

```ts
import { Middleware } from "inngest";

class StepAuditMiddleware extends Middleware.BaseMiddleware {
  id = "step-audit";
  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;
    }
  }
}
```

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