How to Handle Webhooks in Hono
Why Hono for Webhooks
Hono is a lightweight, fast web framework that runs on multiple runtimes: Node.js, Deno, Bun, Cloudflare Workers, and AWS Lambda. Its small footprint and multi-runtime support make it excellent for webhook handlers that need to be deployed to edge functions or serverless platforms.
A minimal Hono webhook handler is concise and readable:
import { Hono } from 'hono';const app = new Hono();
app.post('/webhooks', async (c) => { const body = await c.req.json(); console.log('Webhook received:', body.type);
await processEvent(body);
return c.json({ received: true }); });
export default app; ```
Hono's context object (c) provides access to the request, headers, and helper methods for creating responses. The c.req object wraps the standard Web API Request object, giving you access to .json(), .text(), and .arrayBuffer() for body parsing.
Signature Verification in Hono
For webhook signature verification, access the raw body using c.req.text():
import { Hono } from 'hono';
import { createHmac, timingSafeEqual } from 'crypto';const app = new Hono();
app.post('/webhooks/stripe', async (c) => { const rawBody = await c.req.text(); const signature = c.req.header('stripe-signature') ?? ''; const secret = process.env.STRIPE_WEBHOOK_SECRET!;
// Parse Stripe signature header const elements = Object.fromEntries( signature.split(',').map(part => { const [key, value] = part.split('='); return [key, value]; }) );
const signedPayload = ${elements.t}.${rawBody};
const expected = createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
if (!timingSafeEqual( Buffer.from(expected), Buffer.from(elements.v1) )) { return c.json({ error: 'Invalid signature' }, 401); }
const event = JSON.parse(rawBody); await handleStripeEvent(event);
return c.json({ received: true }); }); ```
When running on Cloudflare Workers or other edge runtimes, use the Web Crypto API instead of Node's crypto module, since Node built-ins are not available in all runtimes.
Middleware for Webhook Security
Hono's middleware system lets you create reusable verification middleware:
import { createMiddleware } from 'hono/factory';const verifyWebhook = (secretEnvKey: string, signatureHeader: string) => createMiddleware(async (c, next) => { const rawBody = await c.req.text(); const signature = c.req.header(signatureHeader);
if (!signature) { return c.json({ error: 'Missing signature' }, 401); }
const secret = process.env[secretEnvKey]; const computed = createHmac('sha256', secret!) .update(rawBody) .digest('hex');
if (signature !== computed) { return c.json({ error: 'Invalid signature' }, 401); }
// Store parsed body for the handler c.set('webhookBody', JSON.parse(rawBody)); await next(); });
// Use the middleware app.post('/webhooks/github', verifyWebhook('GITHUB_WEBHOOK_SECRET', 'x-hub-signature-256'), async (c) => { const body = c.get('webhookBody'); const event = c.req.header('x-github-event'); // Process event... return c.json({ received: true }); } ); ```
This middleware pattern keeps your route handlers clean and focused on business logic while the verification logic is reusable across multiple webhook endpoints.
Edge Runtime Deployment
Hono's multi-runtime support means you can deploy webhook handlers to Cloudflare Workers, Vercel Edge Functions, or Deno Deploy. The handler code is nearly identical across runtimes:
// Works on Cloudflare Workers, Vercel Edge, Deno Deploy
import { Hono } from 'hono';const app = new Hono();
app.post('/webhooks', async (c) => { const body = await c.req.json();
// Use platform-specific APIs for async processing // Cloudflare: c.executionCtx.waitUntil(promise) // Vercel: await promise (within function timeout)
if (c.executionCtx?.waitUntil) { // Cloudflare Workers c.executionCtx.waitUntil(processEventAsync(body)); return c.json({ received: true }); }
await processEvent(body); return c.json({ received: true }); });
export default app; ```
Edge runtimes have shorter timeouts than traditional servers (typically 10-30 seconds). Use waitUntil() on Cloudflare Workers to continue processing after the response is sent. On other platforms, keep processing fast or offload to a queue.
Testing Hono Webhooks with ReqPour
For local development, run your Hono server and use ReqPour to relay webhooks:
# Start your Hono dev server
bun run dev # or npm run dev, deno task dev# In another terminal npx reqpour relay --to http://localhost:3000 ```
Hono provides a built-in test client for unit testing:
import { describe, it, expect } from 'vitest';
import app from './index';describe('Webhook handler', () => { it('processes events', async () => { const res = await app.request('/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'order.created', data: { id: 'ord_123' } }), });
expect(res.status).toBe(200); const body = await res.json(); expect(body.received).toBe(true); }); }); ```
Use payloads captured from the ReqPour dashboard to create test fixtures that match real webhook data from your providers.
Related
Get started with ReqPour
Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.