hooksentinel
Frameworks

Fastify

Integrate hooksentinel with Fastify using a raw content-type parser and the built-in Fastify adapter.

Install

npm install @hooksentinel/core

toFastifyHandler is exported from @hooksentinel/core/fastify.

The raw body requirement

Fastify parses application/json bodies by default, which destroys the raw bytes hooksentinel needs to verify signatures. Register a raw content-type parser scoped to your webhook route(s) so the rest of your app keeps normal JSON parsing.

src/server.ts
import Fastify from 'fastify';
import { toFastifyHandler } from '@hooksentinel/core/fastify';
import { createWebhookHandler, stripe } from '@hooksentinel/core';
import { memoryStore } from '@hooksentinel/core/stores';

const app = Fastify();

app.addContentTypeParser(
  'application/json',
  { parseAs: 'buffer' },
  (_req, body, done) => done(null, body),
);

const stripeWebhook = createWebhookHandler({
  provider: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  idempotency: memoryStore(),
  onEvent: async (event) => {
    if (event.type === 'checkout.session.completed') {
      await fulfillOrder(event.data.object.id);
    }
  },
});

app.post('/webhooks/stripe', toFastifyHandler(stripeWebhook));

async function fulfillOrder(sessionId: string) {
  // ...
}

app.listen({ port: 3000 });

Overriding the global content-type parser to always return a buffer means every route on this Fastify instance now receives a raw Buffer for application/json bodies, not just the webhook route. If you have other JSON routes on the same instance, register hooksentinel's routes on a scoped plugin instead so the override doesn't leak:

src/webhooks/plugin.ts
import type { FastifyPluginAsync } from 'fastify';
import { toFastifyHandler } from '@hooksentinel/core/fastify';
import { stripeWebhook } from './stripe';

export const webhookRoutes: FastifyPluginAsync = async (instance) => {
  instance.addContentTypeParser(
    'application/json',
    { parseAs: 'buffer' },
    (_req, body, done) => done(null, body),
  );

  instance.post('/webhooks/stripe', toFastifyHandler(stripeWebhook));
};
src/server.ts
import Fastify from 'fastify';
import { webhookRoutes } from './webhooks/plugin';

const app = Fastify();
await app.register(webhookRoutes, { prefix: '/webhooks' });

app.listen({ port: 3000 });

Because the raw parser is registered on the scoped plugin instance, routes outside it keep Fastify's normal JSON parsing.

Response behavior

toFastifyHandler returns a Fastify route handler function — pass it directly as the handler argument to app.post(), app.route(), etc. It calls reply.code(...).send(...) internally based on the pipeline outcome; don't write to reply after calling into it.

Multiple providers

Register one route per provider inside the scoped plugin, each pointing at its own createWebhookHandler instance:

instance.post('/stripe', toFastifyHandler(stripeWebhook));
instance.post('/github', toFastifyHandler(githubWebhook));

Last updated on

On this page