DEV Community

Stephen Samra
Stephen Samra

Posted on • Edited on • Originally published at stephensamra.com

Express & Stripe: webhooks done right

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:

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});
});
Enter fullscreen mode Exit fullscreen mode

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:

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...
}
Enter fullscreen mode Exit fullscreen mode

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

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
Enter fullscreen mode Exit fullscreen mode

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

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});
});
Enter fullscreen mode Exit fullscreen mode

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 or Twitter.

Top comments (1)

Collapse
 
chrisimade profile image
@Chrisdevcode

Just found a better way to connect to Stripe in my application. Thanks