Stripe Integration for Remix

Stripe Integration for Remix

Stripe is one of the best payment providers on the market. Their developer experience is almost unmatched. From the ability to test all your scenarios with test clocks to extensive and powerful documentation, it is a hard choice to pass up when it comes to charging your users for the services you offer.

Setting up Stripe is really simple with Remix and in today's article, I will show you how to do just that. Without further ado let's go over setting up Stripe.

I will be assuming you have an existing Remix project set up, if not, take your time

Setup

There are a few things you need to understand about Stripe before jumping into the implementation itself. Stripe offers a powerful API exposed through their npm package called stripe (yes, I know you're shocked about the name).

Setting it up is really easy and simple so let's do it right away. Firstly, add it to your project via npm by running:

npm i stripe

This will install the npm package for you. After it's installed let's create a folder called stripe in our app folder and add a stripe.server.ts file to it. I personally like to keep all my Stripe logic inside of this file.

After this, you should go to your Stripe Dashboard and set up a SECRET_KEY and the STRIPE_WEBHOOK_SIGNING_SECRET that you can use to connect to their API and also listen to webhook events. This is out of the scope of this article but you can find how to do that here:

https://stripe.com/docs/js/initializing

After we add the secret key to our .env file we instantiate the Stripe instance by adding the following code inside our stripe.server.ts

import Stripe from "stripe";
// process.env.STRIPE_SECRET_KEY has been added in the .env
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2023-10-16" });

The stripe object from above is going to be your best friend from here on out, it's an amazing utility that offers you full type-safety to do anything that you want with Stripe entities like checkouts, subscriptions, customers, coupons and so on.

Now let's move on to the next step so we can actually start using this instance!

Webhook

If you don't know what a webhook is the easiest way to understand it is that it's an endpoint you create so a third-party service can send you information when something happens inside their system.

Considering that Stripe is that third-party service we will need a webhook to listen to events on Stripe and handle them accordingly. Stripe offers great documentation on how to set up a webhook of your own, but you have me to help you out! So let's create a webhook.server.ts file inside of your stripe folder and add the following code inside of it:

import Stripe from "stripe";
import { stripe } from "./stripe.server";

const getStripeEventOrThrow = async (request: Request) => {
  const signature = request.headers.get("stripe-signature");
  const payload = await request.text();
  let event: Stripe.Event;

  if (!signature || !payload) {
    throw new Response("Invalid Stripe payload/signature", {
      status: 400
    });
  }
  try {
    event = stripe.webhooks.constructEvent(payload, signature, process.env.STRIPE_WEBHOOK_SIGNING_SECRET);
  } catch (err: any) {
    throw new Response(err.message, {
      status: 400
    });
  }
  return event;
};

If you want to go deeply into how this works you can check the Stripe documentation, but the tl:dr; of it is that we get a stripe-signature header that is present on every event that Stripe sends to our server (if it's not there it wasn't sent by Stripe). We also get the payload as text that was sent and if either of those does not exist we throw a 400 Response.

If they are there, we try to construct an event from Stripe, and if it works we return it to the caller of this function, otherwise, we throw a 400 response again. If this function does not throw it means it's a valid Stripe webhook event and we get full type safety from here on out.

The next step is actually using this somewhere. So right beneath this we add the following piece of code:

/**
 * Handles events from Stripe emitted via webhooks.
 * @param request - The incoming request object.
 */
export const handleStripeWebhook = async (request: Request) => {
  const event = await getStripeEventOrThrow(request);

  // We return null here, you could return a response as well, up to you
  return null;
};

For now, this is very simple, we will circle back to it later, let's first set up an actual webhook. So, as we said, a webhook listens to events from a third party and handles them. In order for us to handle events from Stripe we need to expose an endpoint to where those events will be sent to, and this is where resource routes come into play.

Inside our routes folder let's create a webhook.stripe.ts file and add the following code inside it:

import type { ActionFunctionArgs } from "@remix-run/node";
import { handleStripeWebhook } from "~/stripe/webhook.server";

export const action = ({ request }: ActionFunctionArgs) => handleStripeWebhook(request);

What are we doing here? Well, we simply expose a <url>/webhooks/stripe POST endpoint for Stripe to send us events, we use the handleStripeWebhook to make sure it's an event that came from Stripe and we handle it accordingly.

We are now listening to anything that happens on the Stripe side, and this is important because every time your user does something on Stripe you automatically get an event on your webhook when he pays, when he changes a payment method, when a subscription changes, when a coupon gets added, you're listening to all these events now.

Some of them are not going to be useful for your use-case but a few of them are. For the purpose of this article let's try creating a checkout session with an annual subscription.

First, I want you to go to your Stripe dashboard and create a product with a price, you can do whatever you want here. The important thing is for you to copy the price_id of the object you create.

After you're done go to your stripe.server.ts file and add the following code:

export const createCheckoutSession = async () => {
  // get the user here somehow, either pass him in as a parameter or add
  // another function that fetches him
  const user = { email: "email@email.com" };
  // The id of the price you created in your dashboard, this can also be an
  // argument to this function
  const price = "price_1JQZ2nJZ6X9ZQX2Y0Z2ZQX2Y";
  // Creates a new Checkout Session for the order
  const session = await stripe.checkout.sessions.create({
    // your site url where you want user to land after checkout completed
    success_url: "http://localhost:5173/success",
    // your site url where you want user to land after checkout canceled
    cancel_url: "http://localhost:5173/cancel", 
    // users email, if you create a customer before this step you can assign the customer here too.
    customer_email: user.email,
    // Items to be attached to the subscription
    line_items: [
      {
        price,
        quantity: 1
      }
    ],
    mode: "subscription"
  });
  // The url to redirect the user to for him to pay.
  return session.url;
};

This is very simple but it's for demo purposes, you will probably want something a lot smarter, or not, depending on your application. After this let's create another resource route called stripe.pay.ts in our routes folder and add the following code to it:

import { ActionFunctionArgs, json, redirect } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import { createCheckoutSession } from "~/server/stripe/stripe.server";

export const action = async ({ request }: ActionFunctionArgs) => {
  const url = await createCheckoutSession();
  if (!url) {
    return json({ error: "Something went wrong" }, { status: 500 });
  }
  return redirect(url);
};

/**
 * Used to redirect the user to the Stripe checkout page
 * @returns A custom fetcher with extended submit function
 */
export const useStripeCheckout = () => {
  const fetcher = useFetcher<typeof action>();
  return {
    ...fetcher,
    // overwrites the default submit so you don't have to specify the action or method
    submit: () => fetcher.submit(null, { method: "POST", action: "/stripe/pay" })
  };
};

What this allows us to do is create checkouts inside of our components by directly calling the resource route and redirecting the user, you could extend it to accept price ids and pass them along to your endpoint so you can dynamically add items to your checkout instead of having them hardcoded.

Now in your UI, you can use this hook by doing the following:

import { useStripeCheckout } from "./stripe.pay";

export default function Route() {
  const fetcher = useStripeCheckout();
  return <button onClick={() => fetcher.submit()} />
}

This will create a checkout, redirect the user to Stripe so he can pay, and then send you the webhook event so you can handle it, we are already listening to Stripe webhook events, so let's listen to an event when a subscription is paid.

We add the following code to our webhook.server.ts handleStripeWebhook function we created earlier:

if (event.type === "checkout.session.completed" || event.type === "checkout.session.async_payment_succeeded") {
  // This will trigger every time a user pays for the subscription
  const session = event.data.object;
  // We get the subscription that was created
  const subscription = session.subscription;
  // We somehow store it in our database or do whatever we need to
  await storeSubscription(subscription);
}

And now we are listening to whenever a subscription is created and we store the subscription info somewhere. The best part about this it's fully typesafe so you can check out what you get from their API and figure out what is important for you to store and handle.

There are a million more ways to go about handling subscriptions, payments and user flows and if I wrote 30 pages of article content it still wouldn't fit so I've gone with the basics to go over what you need to know to get you started.

I kindly recommend installing their VS Code Stripe extension and Stripe CLI for development and using test clocks to test your scenarios these will vastly improve your experience with Stripe, you can pipe webhook events directly to your localhost machine and see logs of all the events.

Find the CLI here:

https://stripe.com/docs/stripe-cli

Find the VS Code extension docs here:

https://stripe.com/docs/stripe-vscode

If you are in need of a starter stack with everything you might ever need with Stripe you might want to check out:

https://github.com/dev-xo/stripe-stack

Thank you

If you've reached the end you're a champ! I hope you liked my article.

If you liked the article consider sharing it with others, or tagging me on Twitter with your thoughts, would love to hear them!

If you wish to support me follow me on Twitter here:

twitter.com/AlemTuzlak59192

or if you want to follow my work you can do so on GitHub:

github.com/AlemTuzlak

And you can also sign up for the newsletter to get notified whenever I publish something new!