Web API Native
handle(request) returns a Response, so the router plugs directly into Bun, Deno, Cloudflare Workers, Next.js, Hono, and any other fetch-compatible runtime.
handle(request: Request) returns a Response, so the router plugs directly into Bun, Deno, Cloudflare Workers, Next.js route handlers, Hono, and any other fetch-compatible runtime. There is no adapter layer to implement.
Why this design:
- no framework lock-in and no custom request/response contract to learn
- smaller package surface
- raw body capture is handled by the router itself: it reads the request body exactly once and exposes the exact bytes as
ctx.rawBody, so signature verification always sees what the provider sent
Fetch-Native Runtimes
import { createWebhookRouter } from "@zap-studio/webhooks";
const router = createWebhookRouter();
router.register("/ping", () => Response.json("pong"));
// Bun / Deno / Cloudflare Workers
export default {
fetch: (request: Request) => router.handle(request),
};
Next.js Route Handlers
// app/webhooks/[...path]/route.ts
import { router } from "@/lib/webhooks";
export const POST = (request: Request) => router.handle(request);
Hono
import { Hono } from "hono";
import { router } from "./webhooks";
const app = new Hono();
app.all("/webhooks/*", (c) => router.handle(c.req.raw));
Node http Servers
Raw Node http servers do not speak fetch natively. Use a fetch-to-Node bridge such as srvx or @hono/node-server:
import { serve } from "srvx";
import { router } from "./webhooks";
serve({
fetch: (request) => router.handle(request),
port: 3000,
});
The router parses the request URL’s pathname, requires the configured prefix (default /webhooks) on a segment boundary, and matches routes on the remainder. Prefix, routes, and incoming pathnames are normalized to the same canonical form (leading slash, no trailing slash), so a stray trailing slash never 404s. Requests that miss the prefix or match no route get a 404 without the body ever being read.