DEV Community

Richard for Stripe

Posted on

stripe-node with Deno

This post shows an example of stripe-node usage in a Deno environment. Beyond this post, there's a video tutorial, a Deno example in the stripe-node repository itself, and there is also a Stripe sample that you can read or clone from Github. If you just want to see code, wander over to Github. Stay here if you prefer your code examples with a side of dramatic commentary.

Background

Earlier this year, in stripe-node v11.15.0, we added support to import stripe-node as an ES Module. This means it is much more convenient to use Stripe in non-NodeJS runtime environments, like Cloudflare Workers or Deno. Something funny: "stripe-node" is a bit misnomerous now. Node is not the only option anymore. Maybe stripe-server-js would be a more accurate name, but that doesn't quite roll off the tongue, does it? We'll stick with stripe-node, I'm sure you know what we mean.

Prerequisites

Start your project

If you're starting from scratch, then now it's time to run deno init. If you've already got a Deno project going, way to go! You're ahead of the game, but you might have to adapt the rest of the instructions.

Importing Stripe

Deno init creates several files for you, among them main.ts, the entry point for your app. Open it with your code editor. No need to npm install with Deno. You can simply put

import Stripe from "npm:stripe@^13.0.0"
Enter fullscreen mode Exit fullscreen mode

at the top of your main.ts, and the package will be installed automatically. At the time of this writing, 13.x.x is the latest major line of stripe-node, but if you're from the future you can npm view stripe version to see the latest version.

Note: you will need to import the npm versions of stripe-node. The versions at unpkg.com or deno.land aren't quite working due to how the import statements in our source code are written.

Using Deno's import maps

If you're going to be importing stripe inside multiple files, you might not want to have to update the version in every import statement when it comes time to update the Stripe library. In this case, you will instead want to use a bare specifier and Deno's import maps. Change that import statement:

- import Stripe from "npm:stripe@^13.0.0"
+ import Stripe from "stripe"
Enter fullscreen mode Exit fullscreen mode

and then edit deno.jsonc to add an entry for stripe in the "imports" map.

  {
    "tasks": {
      "dev": "deno run --watch main.ts"
-   }
+   },
+   "imports": {
+     "stripe": "npm:stripe@^13.0.0"
+   }
  }
Enter fullscreen mode Exit fullscreen mode

Voila!

Using Stripe

In Deno, you use Stripe just like you would in any other Typescript codebase. For example, suppose you want to make an API call to list customers. Initialize the Stripe client with your API key, and make the API call.

const stripe = new Stripe("<YOUR (TEST MODE) API KEY GOES HERE>", {
  apiVersion: "2023-08-16",
})

if (import.meta.main) {
  const customers = await stripe.crustomers.list({limit: 5});
  console.log(customers.data.length);
  console.log(customers.data.map(c => c.email));
}
Enter fullscreen mode Exit fullscreen mode

Fetch vs. npm:https

In recent versions of Deno, this will Just Work, because Deno has added compatibility shims for the Node standard library. In versions of Deno before the Node compatibility mode (introduced in v1.15, stabilized in 1.28), you would have had to write

  const stripe = new Stripe("<YOUR API KEY GOES HERE>", {
    apiVersion: "2023-08-16",
+   httpClient: Stripe.createFetchHttpClient(),
  });
Enter fullscreen mode Exit fullscreen mode

to tell stripe-node that you want to use the "fetch" API (native to the web, and to Deno), rather than attempting to use the "node:https" module from the Node standard library (which stripe-node tries to use by default). I recommend do this even if you are on a new version of Deno, though, just to skip the compatibility layer and keep things behind the scenes simpler.

Typecheck it, run it, get permission

Before we run the code, let's type check it:

$ deno check main.ts
error: TS2551 [ERROR]: Property 'crustomers' does not exist on type 'Stripe'. Did you mean 'customers'?
  const customers = await stripe.crustomers.list();
Enter fullscreen mode Exit fullscreen mode

Whoops! An embarassing typo, but at least we know the types are all set up and working. Fix the typo, run deno check main.ts and once we get a clean bill of health, let's go ahead and run our program!

$ deno run main.ts

┌ ⚠️  Deno requests net access to "api.stripe.com".
├ Requested by `fetch()` API.
├ Run again with --allow-net to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >
Enter fullscreen mode Exit fullscreen mode

This is Deno's hallmark permissions system at play. In exchange for a bit of inconvenience, it offers you some peace of mind. If you see "api.stripe.com" as the domain name, nobody has compromised your supply chain with a malicious version of stripe-node that transmits your Stripe credentials to some other domain. Go ahead and press y, and the program will run.

✅ Granted net access to "api.stripe.com".
5
[ "richardm+1@example.com", "richardm+2@example.com", ...]
Enter fullscreen mode Exit fullscreen mode

Now run it with the command-line flag to bypass the prompt.

$ deno run main.ts --allow-net=api.stripe.com
Enter fullscreen mode Exit fullscreen mode

That's quite a mouthful. If you don't want to have to type all that every time, you should create a "task" in your deno.jsonc. There's already one in there called "dev" created by "deno init" that you can repurpose.

  {
    "tasks": {
-     "dev": "deno run --watch main.ts"
+     "dev": "deno run --watch --allow-net=api.strippe.com main.ts"
    }
  }
Enter fullscreen mode Exit fullscreen mode

Now I'm a suspicious person by nature. So I've deliberately typoed the domain name as a test.

$ deno task dev
Task dev deno run --watch --allow-net=api.strippe.com main.ts
Watcher Process started.
┌ ⚠️  Deno requests net access to "api.stripe.com".
├ Requested by `fetch()` API.
├ Run again with --allow-net to bypass this prompt.
└ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >
Enter fullscreen mode Exit fullscreen mode

I press n and

❌ Denied net access to "api.stripe.com".
error: Uncaught Error: An error occurred with our connection to Stripe. Request was retried 1 times.
    at file:///Users/richardm/Library/Caches/deno/npm/registry.npmjs.org/stripe/13.4.0/esm/RequestSender.js:319:37
    at eventLoopTick (ext:core/01_core.js:197:13)
Enter fullscreen mode Exit fullscreen mode

Everything seems to be in order! I fix the typo, run npm task dev again, and everything is hunky-dory! Not only did I confirm the workings of Deno's permission system, but this helps me save face and pretend my previous embarassing typo "crustomers" was a deliberate choice.

Webhooks

Now for completeness, let's listen for webhooks too.

First, run stripe listen to receive events from the Stripe API and direct them to localhost. Let's be cliché and pick port 3000.

$ stripe listen --latest --forward-to=localhost:3000
> Ready! You are using Stripe API Version [2023-08-16]. Your webhook signing secret is whsec_... (^C to quit)
Enter fullscreen mode Exit fullscreen mode

Copy the webhook signing secret, and let's edit main.ts one last time and add the following snippet of code, lightly adapted from the examples in github.com/stripe/stripe-node:

if (import.meta.main) {
  ...
  const port = 3000
  const server = Deno.listen(
    {hostname: '127.0.0.1', port: 3000}
  );
  console.log(
    `Webhook endpoint available at http://127.0.0.1:${port}/
  `);

  async function handler(request: Request) {
    const signature = request.headers.get('Stripe-Signature');

    const body = await request.text();
    let event;
    try {
      event = await stripe.webhooks.constructEventAsync(
        body,
        signature,
        "<YOUR SIGNING SECRET GOES HERE>",
        undefined
      );
    } catch (err) {
      console.log(`❌ Error message: ${err.message}`);
      return new Response(err.message, {status: 400});
    }

    console.log('✅ Success:', event.id);

    if (event.type === 'payment_intent.succeeded') {
      const obj = event.data.object as Stripe.PaymentIntent;
      console.log(`💰 PaymentIntent status: ${obj.status}`);
    } else {
      console.warn(`❌Unhandled event type: ${event.type}`);
    }
    return new Response(
      JSON.stringify({received: true}), {status: 200}
    );
  }
  await serveListener(server, handler);
}

Enter fullscreen mode Exit fullscreen mode

Now time to run it:

$ deno task dev
Task dev deno run --watch --allow-net=api.stripe.com main.ts
...
 ⚠️  Deno requests net access to "127.0.0.1:3000".
 Requested by `Deno.listen()` API.
 Run again with --allow-net to bypass this prompt.
 Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) > ^C
Enter fullscreen mode Exit fullscreen mode

The permissions system is at it again! Let's update deno.jsonc again.

    "tasks": {
-     "dev": "deno run --watch --allow-net=api.stripe.com main.ts"
+     "dev": "deno run --watch --allow-net=api.stripe.com,127.0.0.1:3000 main.ts"
    }
Enter fullscreen mode Exit fullscreen mode

Run deno task dev, and it will run in the background, listening for events. Make sure you've got stripe listen --latest --forward-to=localhost:3000 running in another terminal, and then in a third terminal, run stripe trigger payment_intent.succeeded and you should see your Deno process log output like

Webhook endpoint available at http://127.0.0.1:3000/
✅ Success: evt_3Nnabcdefghijklmnopqrstu
❌ Unhandled event type: payment_intent.created
✅ Success: evt_3Nnabcdefghijklmnopqrstu
❌ Unhandled event type: charge.succeeded
✅ Success: evt_3Nnabcdefghijklmnopqrstu
💰 PaymentIntent status: succeeded
✅ Success: evt_3Nnabcdefghijklmnopqrstu
❌ Unhandled event type: payment_intent.created
Enter fullscreen mode Exit fullscreen mode

What's next?

That wraps it up for the post, but some obvious next steps would be:

  • Use environment variables for your signing secret and API key.
  • Deploy to a server on the Internet instead of localhost, and create a webhook endpoint to send Stripe events to that server.
  • Write the Stripe-related logic you actually need for your app. Listing customers is just the start.

But I believe in you. You've got this. Don't let the haters get you down. I will leave you with these words of inspiration: if you have a suggestion, or something seems wrong with stripe-node's Deno support, you can always leave feedback in a Github issue at github.com/stripe/stripe-node. Good luck!

Top comments (0)