---
title: Lifecycle Hooks
description: Use before, after, and onError hooks for logging, metrics, and centralized error handling.
type: package
package: "@zap-studio/webhooks"
---

`webhooks` exposes global `before`, `after`, and `onError` hooks for cross-cutting behavior. Hooks are shared interception points around route execution — they keep handlers focused on business logic while logging, metrics, and error policy live in one place.

## Add Global Hooks

Configure hooks once in `createWebhookRouter` — they apply to every route. Each of `before` and `after` accepts a single hook or an array; hooks run sequentially in registration order.

```ts
import { createWebhookRouter } from "@zap-studio/webhooks";

const router = createWebhookRouter({
  before: (ctx) => {
    console.log("incoming", ctx.path);
  },
  after: (_ctx, response) => {
    console.log("status", response.status);
  },
  onError: (error) => Response.json({ error: error.message }, { status: 500 }),
});
```

Every hook receives the webhook context — `{ request, rawBody, path }` — where `request` is the incoming Web API `Request` (body already consumed by the router), `rawBody` holds the exact body bytes, and `path` is the matched route key.

Hook signatures (exported from `webhooks`):

- `BeforeHook` — `(ctx: WebhookContext) => Promise<void> | void`
- `AfterHook` — `(ctx: WebhookContext, response: Response) => Promise<void> | void`
- `ErrorHook` — `(error: Error, ctx: WebhookContext) => Response | undefined`, sync or async

:::note

`after` hooks receive the outgoing `Response` as-is. Reading its body would consume the stream sent to the client, so call `response.clone()` first:

```ts
const router = createWebhookRouter({
  after: async (_ctx, response) => {
    const body = await response.clone().json();
    console.log("responded with", body);
  },
});
```

:::

## Add Route-Level Hooks

Register `before` and `after` on a single route when one endpoint needs extra behavior beyond the global hooks.

```ts
import { z } from "zod";

router.register("/github/push", {
  schema: z.object({ ref: z.string() }),
  before: (ctx) => {
    console.log("route-before", ctx.path);
  },
  handler: () => Response.json("ok"),
  after: (_ctx, response) => {
    console.log("route-after", response.status);
  },
});
```

## Understand Execution Order

For a successful request, the router runs:

1. global `before` hooks
2. route `before` hooks
3. `verify`
4. body parsing and schema validation
5. handler
6. route `after` hooks
7. global `after` hooks

`after` hooks only run when the handler completes successfully. If any step throws, execution stops and error handling takes over.

:::note

`before` hooks run **before** verification, so they see requests that have not been authenticated yet. Keep side effects out of `before` hooks — use them for logging, tracing, or rate limiting, and treat the request as untrusted until `verify` has passed.

:::

## Centralize Error Handling

When any hook, the verifier, or the handler throws, the router calls `onError` with the error and the webhook context. Return a `Response` to override the default response, or return `undefined` to fall back to the default `500` with `{ error: message }`.

```ts
import { createWebhookRouter, VerificationError } from "@zap-studio/webhooks";

const router = createWebhookRouter({
  onError: (error, ctx) => {
    console.error("webhook failed", ctx.path, error.message);

    if (error instanceof VerificationError) {
      return Response.json({ error: "invalid signature" }, { status: 401 });
    }

    return undefined; // default 500 response
  },
});
```

`onError` gives you one place to standardize error shape and status codes, and to map provider or framework errors consistently. Validation failures do not reach `onError` — the router returns a `400` with the validation issues directly.
