🎣better-webhook
SDK

Framework Adapters

Integrate Better Webhook with Next.js, Express, NestJS, and GCP Cloud Functions.

Framework Adapters

Adapters convert the webhook builder into framework-specific handlers. Each adapter handles the request/response lifecycle and passes the raw body for signature verification.

Next.js

bash npm install @better-webhook/nextjs
bash pnpm add @better-webhook/nextjs
bash yarn add @better-webhook/nextjs

The Next.js adapter works with the App Router's route handlers.

Basic Usage

app/api/webhooks/github/route.ts
import { github } from "@better-webhook/github";
import { toNextJS } from "@better-webhook/nextjs";

const webhook = github().event("push", async (payload) => {
  console.log(`Push to ${payload.repository.name}`);
});

export const POST = toNextJS(webhook);

With Options

export const POST = toNextJS(webhook, {
  // Override the secret (instead of using provider or env var)
  secret: process.env.GITHUB_WEBHOOK_SECRET,

  // Callback after successful processing
  onSuccess: async (eventType) => {
    console.log(`Successfully processed ${eventType}`);
  },
});

Response Behavior

StatusCondition
200Handler executed successfully
204No handler registered for this event type
400Invalid JSON body or schema validation failed
401Signature verification failed
405Request method is not POST
500Handler threw an error

Express

bash npm install @better-webhook/express
bash pnpm add @better-webhook/express
bash yarn add @better-webhook/express

The Express adapter returns a middleware function.

Basic Usage

src/webhooks.ts
import express from "express";
import { github } from "@better-webhook/github";
import { toExpress } from "@better-webhook/express";

const app = express();

const webhook = github().event("push", async (payload) => {
  console.log(`Push to ${payload.repository.name}`);
});

// Important: use express.raw() for this route
app.post(
  "/webhooks/github",
  express.raw({ type: "application/json" }),
  toExpress(webhook),
);

app.listen(3000);

Important: You must use express.raw({ type: "application/json" }) before the webhook middleware. Without the raw body, signature verification will fail.

With Options

app.post(
  "/webhooks/github",
  express.raw({ type: "application/json" }),
  toExpress(webhook, {
    secret: process.env.GITHUB_WEBHOOK_SECRET,
    onSuccess: async (eventType) => {
      console.log(`Processed ${eventType}`);
    },
  }),
);

Multiple Providers

import { github } from "@better-webhook/github";
import { ragie } from "@better-webhook/ragie";
import { toExpress } from "@better-webhook/express";

const githubWebhook = github().event("push", async (payload) => {
  // Handle GitHub
});

const ragieWebhook = ragie().event(
  "document_status_updated",
  async (payload) => {
    // Handle Ragie
  },
);

app.post(
  "/webhooks/github",
  express.raw({ type: "application/json" }),
  toExpress(githubWebhook),
);

app.post(
  "/webhooks/ragie",
  express.raw({ type: "application/json" }),
  toExpress(ragieWebhook),
);

NestJS

bash npm install @better-webhook/nestjs
bash pnpm add @better-webhook/nestjs
bash yarn add @better-webhook/nestjs

The NestJS adapter returns an async function that processes the request and returns a result object.

Basic Usage

src/webhooks.controller.ts
import { Controller, Post, Req, Res } from "@nestjs/common";
import { Request, Response } from "express";
import { github } from "@better-webhook/github";
import { toNestJS } from "@better-webhook/nestjs";

@Controller("webhooks")
export class WebhooksController {
  private webhook = github().event("push", async (payload) => {
    console.log(`Push to ${payload.repository.name}`);
  });

  @Post("github")
  async handleGitHub(@Req() req: Request, @Res() res: Response) {
    const result = await toNestJS(this.webhook)(req);
    return res.status(result.statusCode).json(result.body);
  }
}

Raw Body Configuration

For signature verification to work, NestJS must preserve the raw request body. Enable this in your main.ts:

src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    rawBody: true, // Enable raw body
  });
  await app.listen(3000);
}
bootstrap();

Without rawBody: true, the adapter will attempt to re-serialize the parsed body, which may not match the original and cause signature verification to fail.

With Options

@Post("github")
async handleGitHub(@Req() req: Request, @Res() res: Response) {
  const handler = toNestJS(this.webhook, {
    secret: process.env.GITHUB_WEBHOOK_SECRET,
    onSuccess: async (eventType) => {
      console.log(`Processed ${eventType}`);
    },
  });

  const result = await handler(req);
  return res.status(result.statusCode).json(result.body);
}

Result Object

The NestJS adapter returns a result object instead of directly sending a response:

interface NestJSResult {
  statusCode: number;
  body?: Record<string, unknown>;
}

This gives you control over the response, allowing you to add headers, transform the body, or perform additional logic before responding.


GCP Cloud Functions

bash npm install @better-webhook/gcp-functions
bash pnpm add @better-webhook/gcp-functions
bash yarn add @better-webhook/gcp-functions

The GCP Cloud Functions adapter works with both 1st and 2nd generation Cloud Functions.

Basic Usage (2nd Gen)

index.ts
import { http } from "@google-cloud/functions-framework";
import { ragie } from "@better-webhook/ragie";
import { toGCPFunction } from "@better-webhook/gcp-functions";

const webhook = ragie().event("document_status_updated", async (payload) => {
  console.log(`Document ${payload.document_id} is now ${payload.status}`);
});

http("webhookHandler", toGCPFunction(webhook));

Basic Usage (1st Gen)

index.ts
import { ragie } from "@better-webhook/ragie";
import { toGCPFunction } from "@better-webhook/gcp-functions";

const webhook = ragie().event("document_status_updated", async (payload) => {
  console.log(`Document ${payload.document_id} is now ${payload.status}`);
});

export const webhookHandler = toGCPFunction(webhook);

With Options

http(
  "webhookHandler",
  toGCPFunction(webhook, {
    secret: process.env.RAGIE_WEBHOOK_SECRET,
    onSuccess: async (eventType) => {
      console.log(`Processed ${eventType}`);
    },
  }),
);

Raw Body for Signature Verification

GCP Cloud Functions with the Functions Framework provide req.rawBody automatically. The adapter checks for raw body in this order:

  1. req.rawBody (Functions Framework default)
  2. Buffer body
  3. String body
  4. JSON.stringify(req.body) as fallback

If using a custom setup without raw body preservation, signature verification may fail due to JSON serialization differences.

Deployment

Deploy using gcloud CLI:

gcloud functions deploy webhookHandler \
  --gen2 \
  --runtime nodejs20 \
  --trigger-http \
  --allow-unauthenticated \
  --entry-point webhookHandler \
  --set-env-vars RAGIE_WEBHOOK_SECRET=your-secret

Adapter Options

All adapters accept the same options:

OptionTypeDescription
secretstringWebhook secret for signature verification. Overrides provider secret and environment variables.
onSuccess(eventType: string) => void | Promise<void>Callback invoked after successful webhook processing. Errors from this callback are ignored.
observerWebhookObserver | WebhookObserver[]Observer(s) for webhook lifecycle events. Add metrics, logging, or tracing without modifying the webhook builder.

Secret Resolution Order

When verifying signatures, the SDK looks for a secret in this order:

  1. Adapter options — toNextJS(webhook, { secret: "..." })
  2. Provider options — github({ secret: "..." })
  3. Environment variables — GITHUB_WEBHOOK_SECRET, RAGIE_WEBHOOK_SECRET, or WEBHOOK_SECRET

If no secret is found, signature verification is skipped.

Always configure a secret in production. Without signature verification, anyone can send fake webhooks to your endpoint.

Adding Observability via Adapters

You can add observers at the adapter level to track metrics without modifying your webhook builder:

import { createWebhookStats } from "@better-webhook/core";

const stats = createWebhookStats();

// Next.js
export const POST = toNextJS(webhook, {
  observer: stats.observer,
});

// Express
app.post(
  "/webhooks/github",
  express.raw({ type: "application/json" }),
  toExpress(webhook, { observer: stats.observer }),
);

// NestJS
const result = await toNestJS(this.webhook, {
  observer: stats.observer,
})(req);

// GCP Cloud Functions
http(
  "webhookHandler",
  toGCPFunction(webhook, {
    observer: stats.observer,
  }),
);

See the SDK Getting Started page for more details on observability.

On this page