DEV Community

Stephen Samra
Stephen Samra

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

2

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.

Image of Datadog

The Future of AI, LLMs, and Observability on Google Cloud

Datadog sat down with Google’s Director of AI to discuss the current and future states of AI, ML, and LLMs on Google Cloud. Discover 7 key insights for technical leaders, covering everything from upskilling teams to observability best practices

Learn More

Top comments (1)

Collapse
 
chrisimade profile image
@Chrisdevcode

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

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up