Navigation Menu

Web Dev Logics
← Back to blog

Published on by Michael Wong

Stripe, Elysia and Bun

Anyone starting an online business or selling products on the internet needs to learn the basics of building a SaaS(Software as a Service) application. In this guide, you’ll learn how to set up a basic payment system with Stripe, focusing on implementing checkout sessions and handling webhooks. Stripe has great guides using React, Next JS and HTML + Vanilla JS. I’m going to show you how to use Stripe in Elysia.

Prerequisites

To follow along this guide, you will need the following:

Optional

Steps

  1. Create a new Elysia application
  2. Create a Hurl file
  3. Add Environment Variables
  4. Create a New Product
  5. Add Checkout Route
  6. Add Checkout Frontend
  7. Add the Stripe CLI
  8. Add Webhook Route

Create a new Elysia application

ElysiaJS is a modern web framework designed for building backend servers using TypeScript.

For this guide, I have created a Github repo that you can find the finished application.

Let’s start by creating a new Elysia application.

bun create elysia stripe-elysia

Once that’s done, you can move into the project directory and start the app:

cd stripe-elysia
bun run dev

Create a Hurl file

Hurl is a command line tool that runs HTTP requests defined in a simple plain text format.

In the root of the project, create a new Hurl file called stripe.hurl.

├── src
│   ├── index.ts
├── README.md
├── bun.lockb
├── package.json
├── stripe.hurl
└── tsconfig.json
# File: stripe.hurl

GET http://localhost:3000/

The statement above will send a request to the http://localhost:3000/ route.

  • Run the Hurl file, make sure you Elysia app is running and run the following command in another terminal:
hurl stripe.hurl

You should see the following output: Hello Elysia

Add Environment Variables

An .env file is used to store sensitive information like API keys, secrets and database credentials.

In the root of the project, create a new .env file.

# File: .env

STRIPE_PUBLISHABLE_KEY=pk_test_
STRIPE_SECRET_KEY=sk_test_
STRIPE_WEBHOOK_SECRET=whsec_
├── src
│   ├── index.ts
├── README.md
├── bun.lockb
├── .env
├── package.json
├── stripe.hurl
└── tsconfig.json
  • Get the Publishable and Secret key from Stripe. We’ll get the Webhook Secret later.
  • Add the STRIPE_PUBLISHABLE_KEY and STRIPE_SECRET_KEY to the .env file.

Stripe Keys

Create a New Product

  • Add a new product to Stripe.
    • Make sure your are in Test Mode on stripe
    • Make sure your product is One-off

Stripe Add Product

Make note of the price ID, we need it for the checkout route.

Stripe Copy Price ID

Add Checkout Route

First, let’s install the stripe package.

bun add stripe

Back to your project, let’s create a POST route to handle the checkout.

  • The following code will create a new checkout session and return the session URL
    • Update the line_items to match the price ID from you Stripe Dashboard
// index.ts

import { Elysia } from 'elysia'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
  apiVersion: '2024-06-20',
})

const app = new Elysia()
  .get('/', () => 'Hello Elysia')
  .post('/checkout', async () => {
    try {
      const session = await stripe.checkout.sessions.create({
        mode: 'payment',
        line_items: [
          {
            price: '<PRICE_ID>',
            quantity: 1,
          },
        ],
        payment_method_types: ['card'],
        success_url: 'http://localhost:3000/success',
        cancel_url: 'http://localhost:3000/cancel',
      })
      return { url: session.url }
    } catch (error) {
      console.log(error)
      throw new Error('Error in Stripe')
    }
  })
  .get('/success', () => 'Success')
  .get('/cancel', () => 'Cancelled')
  .listen(3000)

console.log(
  `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
)
  • Update the stripe.hurl file
# File: stripe.hurl

POST http://localhost:3000/checkout
  • Run the Hurl file, make sure you Elysia app is running and run the following command in another terminal:
hurl stripe.hurl
  • You should see a URL output, copy the URL and paste it in your browser
{"url": https://checkout.stripe.com/c/pay/cs_test_...}
  • You should see your Stripe checkout page

Stripe Checkout

  • Stripe provides Test Cards
    • Use one of the cards and fill out the form, if successful, you’ll be redirected to the success page

Add Checkout Frontend

Elysia provides some plugins for working with html/jsx and using static files on the server. Install the plugins by running the following command at the root of the project:

bun add @elysiajs/html @elysiajs/static

To use JSX your file needs to end with affix “x”:

  • Update index.ts -> index.tsx

  • Register the Typescript type, append the following to tsconfig.json

// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "Html.createElement",
    "jsxFragmentFactory": "Html.Fragment"
  }
}
  • Create a new file called public/checkout-button.ts
    • This following code adds an event listener to the checkoutButton
    • Change the line_items to match the price ID from you Stripe Dashboard
// public/checkout-button.ts

const checkoutButton = document.getElementById('checkoutButton')
checkoutButton?.addEventListener('click', async () => {
  const response = await fetch('/checkout', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
  })
  const { url } = await response.json()
  window.location = url
})

Update index.tsx

  • To the ’/’ route, we add a button that will trigger the ‘/checkout’ route
// index.tsx

import { Elysia } from "elysia";
import { html, Html } from "@elysiajs/html";
import { staticPlugin } from "@elysiajs/static";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-06-20",
});

const app = new Elysia()
  .use(staticPlugin())
  .use(html())
  .get('/', () => (
    <html lang='en'>
      <title>Checkout</title>
      <script src='https://js.stripe.com/v3/'></script>
      <body>
        <h1>Checkout</h1>
        <button id='checkoutButton'>Checkout</button>
        <script src='/public/checkout-button.ts'></script>
      </body>
    </html>
  ))
  .post('/checkout', async () => {
    try {
      const session = await stripe.checkout.sessions.create({
        mode: 'payment',
        line_items: [
          {
            price: '<PRICE_ID>',
            quantity: 1,
          },
        ],
        payment_method_types: ['card'],
        success_url: 'http://localhost:3000/success',
        cancel_url: 'http://localhost:3000/cancel',
      });
      return { url: session.url };
    } catch (error) {
      console.log(error);
      throw new Error('Error in Stripe');
    }
  })
.get('/success', () => 'Success')
.get('/cancel', () => 'Cancel')
.listen(3000);

console.log(
  `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`,
);

Add the Stripe CLI

stripe login
  • Complete the login process

  • Stripe listen

    • In the terminal, run the following command
stripe listen -e checkout.session.completed --forward-to localhost:3000/webhook
  • Copy the webhook signing secret from the terminal and add it to the .env file

Add Webhook Route

  • append the POST route to index.tsx
  • this route will listen to events in your Stripe account on your webhook endpoint so your integration can automatically trigger reactions.
// index.tsx

.post("/webhook", async ({ set, request }) => {
    const rawBody = await request.text();
    const signature = request.headers.get("stripe-signature");

    let event;

    try {
      event = await stripe.webhooks.constructEventAsync(
        rawBody,
        signature!,
        process.env.STRIPE_WEBHOOK_SECRET!,
      );
    } catch (err) {
      console.log(`❌ Error message: ${err}`);
      set.status = 400;
      throw new Error(`Webhook Error: ${err}`);
    }

    switch (event.type) {
      case "checkout.session.completed": {
        const session = event.data.object;
        console.log("💰 Payment received!", session);
        break;
      }
    // case statements for other event types
      default:
        console.log(`🤷‍♀️ Unhandled event type: ${event.type}`);
    }
    set.status = 200;
    return "success";
  })

  • Make sure your stripe terminal is open and listening for the webhook events
stripe listen -e checkout.session.completed --forward-to localhost:3000/webhook
  • Start your project
bun run dev
  • Fill out the checkout form
    • In your webhook terminal, you should see logs from the events that were triggered by the webhook

Congratulations! You have successfully integrated Stripe into your Elysia app.

Written by Michael Wong

← Back to blog