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
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
| Status | Condition |
|---|---|
200 | Handler executed successfully |
204 | No handler registered for this event type |
400 | Invalid JSON body or schema validation failed |
401 | Signature verification failed |
405 | Request method is not POST |
500 | Handler 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
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
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:
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)
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)
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:
req.rawBody(Functions Framework default)- Buffer body
- String body
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-secretAdapter Options
All adapters accept the same options:
| Option | Type | Description |
|---|---|---|
secret | string | Webhook 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. |
observer | WebhookObserver | 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:
- Adapter options —
toNextJS(webhook, { secret: "..." }) - Provider options —
github({ secret: "..." }) - Environment variables —
GITHUB_WEBHOOK_SECRET,RAGIE_WEBHOOK_SECRET, orWEBHOOK_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.