npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tayori/express

v0.0.1

Published

Express adapter for Tayori Webhook Router

Downloads

16

Readme

@tayori/express

Express framework adapter for Tayori webhook router.

Overview

@tayori/express provides a seamless integration between Tayori's type-safe webhook routing and the Express web framework. Built for Node.js applications with the familiar Express middleware pattern.

Installation

npm install @tayori/express @tayori/core express
# or
pnpm add @tayori/express @tayori/core express
# or
yarn add @tayori/express @tayori/core express

Note: Both @tayori/core and express are peer dependencies and must be installed separately.

Features

  • Express Middleware: Integrates seamlessly with Express request/response cycle
  • Type-Safe: Full TypeScript support with Express types
  • Error Handling: Built-in error handling with customizable responses
  • Raw Body Support: Automatically handles raw request bodies for signature verification
  • Flexible Configuration: Customize success responses and error handling

Quick Start

With Stripe Webhooks

import express from 'express';
import Stripe from 'stripe';
import { StripeWebhookRouter, createStripeVerifier } from '@tayori/stripe';
import { expressAdapter } from '@tayori/express';

const stripe = new Stripe(process.env.STRIPE_API_KEY!);
const router = new StripeWebhookRouter();

router.on('payment_intent.succeeded', async (event) => {
  console.log('Payment succeeded:', event.data.object.id);
});

router.on('customer.subscription.created', async (event) => {
  console.log('New subscription:', event.data.object.id);
});

const app = express();

// IMPORTANT: Use express.raw() to get the raw body for signature verification
app.post('/webhook',
  express.raw({ type: 'application/json' }),
  expressAdapter(router, {
    verifier: createStripeVerifier(stripe, process.env.STRIPE_WEBHOOK_SECRET!),
  })
);

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

With Custom Webhooks

import express from 'express';
import { WebhookRouter, type Verifier, type WebhookEvent } from '@tayori/core';
import { expressAdapter } from '@tayori/express';

// Define your event types
interface MyEvent extends WebhookEvent {
  type: 'my.event';
  data: { object: { id: string; message: string } };
}

type MyEventMap = {
  'my.event': MyEvent;
};

// Create a custom verifier
const myVerifier: Verifier = (payload, headers) => {
  // Verify signature and parse payload
  const body = JSON.parse(payload.toString());
  return {
    event: {
      id: body.id,
      type: body.type,
      data: { object: body.data },
    },
  };
};

const router = new WebhookRouter<MyEventMap>();

router.on('my.event', async (event) => {
  console.log('Custom event:', event.data.object.message);
});

const app = express();

app.post('/webhook',
  express.raw({ type: 'application/json' }),
  expressAdapter(router, {
    verifier: myVerifier,
  })
);

app.listen(3000);

API Reference

expressAdapter

Creates an Express middleware from a Tayori webhook router.

function expressAdapter<TEventMap>(
  router: WebhookRouter<TEventMap>,
  options: ExpressAdapterOptions
): RequestHandler

Parameters:

  • router - A WebhookRouter instance from @tayori/core
  • options - Configuration options

Returns: An Express RequestHandler middleware

ExpressAdapterOptions

interface ExpressAdapterOptions {
  /**
   * Verifier function for webhook signature validation
   * @required
   */
  verifier: Verifier;

  /**
   * Custom error handler
   * @optional
   */
  onError?: (error: Error, event?: WebhookEvent) => Promise<void> | void;

  /**
   * Custom success response
   * @optional
   * @default { success: true }
   */
  successResponse?: unknown;
}

Important: Raw Body Requirement

For webhook signature verification to work, you must use express.raw() middleware before the webhook handler:

// ✅ Correct - raw body is available for verification
app.post('/webhook',
  express.raw({ type: 'application/json' }),
  expressAdapter(router, { verifier })
);

// ❌ Wrong - body will be parsed as JSON, verification will fail
app.use(express.json());
app.post('/webhook', expressAdapter(router, { verifier }));

Mixing JSON and Raw Body Routes

If your app has other routes that need JSON parsing, you have two main options:

Option 1: Webhook Route Before Global JSON Middleware

Define webhook routes before applying express.json() globally:

const app = express();

// ✅ Webhook route FIRST with raw body parser
app.post('/webhook',
  express.raw({ type: 'application/json' }),
  expressAdapter(router, { verifier })
);

// Then apply JSON parser for other routes
app.use(express.json());

// Other JSON routes
app.post('/api/users', (req, res) => {
  // req.body is parsed JSON
  res.json({ success: true });
});

app.listen(3000);

Option 2: Separate Routers for Different Body Parsers

Use separate express.Router() instances with different body parsers:

const app = express();

// Webhook router with raw body parser
const webhookRouter = express.Router();
webhookRouter.post('/webhook',
  express.raw({ type: 'application/json' }),
  expressAdapter(router, { verifier })
);

// API router with JSON body parser
const apiRouter = express.Router();
apiRouter.use(express.json());
apiRouter.post('/users', (req, res) => {
  // req.body is parsed JSON
  res.json({ success: true });
});

// Mount both routers
app.use(webhookRouter);      // Webhook routes at root level
app.use('/api', apiRouter);  // API routes under /api

app.listen(3000);

Advanced Usage

Custom Error Handling

app.post('/webhook',
  express.raw({ type: 'application/json' }),
  expressAdapter(router, {
    verifier: createStripeVerifier(stripe, secret),
    onError: async (error, event) => {
      // Log to monitoring service
      console.error(`Webhook error for ${event?.type}:`, error);

      // Send to error tracking
      await Sentry.captureException(error, {
        tags: {
          eventType: event?.type,
          eventId: event?.id,
        },
      });
    },
  })
);

Custom Success Response

app.post('/webhook',
  express.raw({ type: 'application/json' }),
  expressAdapter(router, {
    verifier: createStripeVerifier(stripe, secret),
    successResponse: {
      status: 'ok',
      processed: true,
      timestamp: Date.now(),
    },
  })
);

Multiple Webhook Endpoints

const app = express();

// Stripe webhooks
const stripeRouter = new StripeWebhookRouter();
stripeRouter.on('payment_intent.succeeded', async (event) => {
  // Handle payment
});

app.post('/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  expressAdapter(stripeRouter, {
    verifier: createStripeVerifier(stripe, stripeSecret),
  })
);

// GitHub webhooks
const githubRouter = new WebhookRouter<GitHubEventMap>();
githubRouter.on('push', async (event) => {
  // Handle push
});

app.post('/webhooks/github',
  express.raw({ type: 'application/json' }),
  expressAdapter(githubRouter, {
    verifier: createGitHubVerifier(githubSecret),
  })
);

app.listen(3000);

With Express Router

import { Router } from 'express';

const webhookRouter = Router();

const stripeRouter = new StripeWebhookRouter();
stripeRouter.on('charge.succeeded', async (event) => {
  console.log('Charge:', event.data.object.id);
});

webhookRouter.post('/stripe',
  express.raw({ type: 'application/json' }),
  expressAdapter(stripeRouter, {
    verifier: createStripeVerifier(stripe, secret),
  })
);

const app = express();
app.use('/webhooks', webhookRouter);
app.listen(3000);

Error Responses

The adapter automatically returns appropriate HTTP responses:

Success (200 OK)

{
  "success": true
}

Verification Failed (401 Unauthorized)

{
  "error": "Webhook verification failed"
}

Handler Error (500 Internal Server Error)

{
  "error": "Webhook processing failed"
}

Testing

Unit Testing with Supertest

import { describe, it, expect } from 'vitest';
import express from 'express';
import request from 'supertest';
import { StripeWebhookRouter } from '@tayori/stripe';
import { expressAdapter } from '@tayori/express';

describe('Webhook handler', () => {
  it('processes payment_intent.succeeded events', async () => {
    const router = new StripeWebhookRouter();
    let processed = false;

    router.on('payment_intent.succeeded', async () => {
      processed = true;
    });

    const app = express();
    app.post('/webhook',
      express.raw({ type: 'application/json' }),
      expressAdapter(router, {
        verifier: mockVerifier,
      })
    );

    const response = await request(app)
      .post('/webhook')
      .send(mockPayload)
      .expect(200);

    expect(response.body).toEqual({ success: true });
    expect(processed).toBe(true);
  });
});

Integration Testing with Stripe CLI

Test with the Stripe CLI for realistic webhook events:

# Terminal 1: Start your Express server
npm start

# Terminal 2: Forward webhooks from Stripe CLI
stripe listen --forward-to localhost:3000/webhook

# Terminal 3: Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created

Common Patterns

Async Processing with Queues

import { Queue } from 'bullmq';

const queue = new Queue('webhooks');

router.on('payment_intent.succeeded', async (event) => {
  // Add to queue for async processing
  await queue.add('process-payment', {
    paymentIntentId: event.data.object.id,
    amount: event.data.object.amount,
  });
});

Database Integration

import { db } from './database';

router.on('customer.subscription.created', async (event) => {
  const subscription = event.data.object;

  await db.subscription.create({
    data: {
      stripeId: subscription.id,
      customerId: subscription.customer,
      status: subscription.status,
      currentPeriodEnd: new Date(subscription.current_period_end * 1000),
    },
  });
});

Requirements

  • Node.js >= 18
  • TypeScript >= 5.3
  • Express >= 4.0.0

Related Packages

Documentation

For more examples and guides, see the main documentation.

License

MIT