Express & Stripe: webhooks done right
Tags: javascript, node, express, stripe
Published: 2023-10-23 19:34:27

When integrating Stripe into an Express application, there's a good chance you'll need to handle Stripe's webhooks to keep your application data in sync with Stripe's data. This article will show you how to organise your webhook handling code to make it easier to maintain and extend.

Some of the examples you'll find online (including Stripe's own docs) will show you how to handle Stripe webhooks using an `if` or `switch` statement to handle each event type:

```js
app.post('/webhooks/stripe', (request, response) => {
  const event = request.body;

  // validate the request...

  if (event.type === 'payment_intent.succeeded') {
    try {
        // handle payment intent succeeded event...
    } catch (err) {
        return response.status(500).json({success: false});
    }
  } else if (event.type === 'payment_method.attached') {
      try {
        // handle payment method attached event...
      } catch (err) {
        return response.status(500).json({success: false});
      }
  } else {
    console.log(`Unhandled event type ${event.type}`);
  }

  response.json({success: true});
});
```

This approach works, but having all of this logic inside of an `if` statement is not ideal. It can become unwieldy and awkward to maintain as you add more event handlers. It also makes it difficult to see, at a glance, what events are being handled by your application.

Let's refactor this code address these issues.

The first improvement we can make is to extract each event handler into its own function:

```js
app.post('/webhooks/stripe', (request, response) => {
  const event = request.body;

  // validate the request...

  if (event.type === 'payment_intent.succeeded') {
    try {
      handlePaymentIntentSucceeded(event);
    } catch (err) {
      return response.status(500).json({success: false});
    }
  } else if (event.type === 'payment_method.attached') {
    try {
      handlePaymentMethodAttached(event);
    } catch (err) {
      return response.status(500).json({success: false});
    }
  } else {
    console.log(`Unhandled event type ${event.type}`);
  }

  response.json({success: true});
});

function handlePaymentIntentSucceeded(event) {
  // handle payment intent succeeded event...
}

function handlePaymentMethodAttached(event) {
  // handle payment method attached event...
}
```

With these function in place, we can eliminate the `if` block and use an object to map event types to event handlers instead:

```js
const handlers = {
  'payment_intent.succeeded': handlePaymentIntentSucceeded,
  'payment_method.attached': handlePaymentMethodAttached,
  // ...
};

app.post('/webhooks/stripe', (request, response) => {
  const event = request.body;

  // validate the request...

  const handler = handlers[event.type] || handleUnhandledEvent;

  try {
    handler(event);
  } catch (err) {
    return response.status(500).json({success: false});
  }

  response.json({success: true});
});

function handlePaymentIntentSucceeded(event) {
  // handle payment intent succeeded event...
}

function handlePaymentMethodAttached(event) {
  // handle payment method attached event...
}

function handleUnhandledEvent(event) {
  console.log(`Unhandled event type ${event.type}`);
}
```

This is much better. Now, all that's required to add a new event handler is to add an entry to the `handlers` object and implement the corresponding function. We can also clearly see what events are handled by our application and where to go if one of them needs to be updated.

If you were to stop reading here, you'd be in good shape. However, there's one more improvement we can make.

Instead of using global variables to store the event map and handler functions, we can encapsulate them in a class, in a separate file:

```js
class StripeWebhookHandler {
  constructor() {
    this.handlers = {
      'payment_intent.succeeded': this.handlePaymentIntentSucceeded,
      'payment_method.attached': this.handlePaymentMethodAttached,
      // ...
    };
  }

  handleEvent(event) {
    const handler = this.handlers[event.type] || this.handleUnhandledEvent;

    handler(event);
  }

  handlePaymentIntentSucceeded(event) {
    // handle payment intent succeeded event...
  }

  handlePaymentMethodAttached(event) {
    // handle payment method attached event...
  }

  // ...

  handleUnhandledEvent(event) {
    console.log(`Unhandled event type ${event.type}`);
  }
}

export default StripeWebhookHandler;
```

Back in our Express route, we can instantiate `StripeWebhookHandler` and use the `handleEvent` method to handle the events:

```js
import StripeWebhookHandler from './stripe-webhook-handler';

const stripeWebhookHandler = new StripeWebhookHandler();

app.post('/webhooks/stripe', (request, response) => {
  const event = request.body;

  // validate the request...

  try {
    stripeWebhookHandler.handleEvent(event);
  } catch (err) {
    return response.status(500).json({success: false});
  }

  response.json({success: true});
});
```

This has the added benefit of separating the Express route from the webhook handling logic; the route is now only concerned with validating the request, passing it to the webhook handler, and returning a response. Updating the webhook handler no longer requires touching the Express route, and vice versa.

I hope you found this article useful. If you have any questions or feedback, let's chat in the comments below. Or, you can always reach me on [Bluesky](https://bsky.app/profile/stephensamra.com) or [Twitter](https://twitter.com/stephensamra_).