Let’s Build a JAMstack E-Commerce Store with Netlify Functions

Avatar of Sarah Drasner
Sarah Drasner on (Updated on )

A lot of people are confused about what JAMstack is. The acronym stands for JavaScript, APIs, and Markup, but truly, JAMstack doesn’t have to include all three. What defines JAMstack is that it’s served without web servers. If you consider the history of computing, this type of abstraction isn’t unnatural; rather it’s the inevitable progression this industry has been moving towards.

So, if JAMstack tends to be static by definition, it can’t have dynamic functionality, server-side events, or use a JavaScript framework, right? Thankfully, not so. In this tutorial, we’ll set up a JAMstack e-commerce app and add some serverless functionality with Netlify Functions (which abstract AWS Lambda, and are super dope in my opinion).

I’ll show more directly how the Nuxt/Vue part was set up in a follow-up post, but for now we’re going to focus on the Stripe serverless function. I’ll show you how I set this one up, and we’ll even talk about how to connect to other static site generators such as Gatsby. Full disclosure, I work for Netlify, and am using their tools for this, it is possible to connect to Stripe with other services. I chose to work for Netlify in part because I enjoy some of the nice abstractions that their services offer.

This site and repo should get you started if you’d like to set something like this up yourself:

Scaffold our app

The very first step is to set up our app. This one is built with Nuxt to create a Vue app, but you can replace these commands with your tech stack of choice:

yarn create nuxt-app

hub create

git add -A
git commit -m “initial commit”

git push -u origin master

I am using yarn, hub (which allows me to create repos from the command line) and Nuxt. You may need to install these tools locally or globally before proceeding.

With these few commands, following the prompts, we can set up an entirely new Nuxt project as well as the repo.

If we log into Netlify and authenticate, it will ask us to pick a repo:

choose a repo in netlify

I’ll use yarn generate to create the project. With that, I can add in the site settings for Nuxt in the dist directory and hit feploy! That’s all it takes to set up CI/CD and deploy the site! Now every time I push to the master branch, not only will I deploy, but I’ll be given a unique link for that particular deploy as well. So awesome.

A basic serverless function with Netlify

So here’s the exciting part, because the setup for this kind of functionality is so quick! If you’re unfamiliar with Serverless, you can think of it like the same JavaScript functions you know and love, but executed on the server. Serverless functions are event-driven logic and their pricing is extremely low (not just on Netlify, but industry-wide) and scales with your usage. And yes, we have to add the qualifier here: serverless still uses servers, but babysitting them is no longer your job. Let’s get started.

Our very basic function looks like this. I stored mine in a folder named functions, and just called it index.js. You can truly call the folder and function what you want.

// functions/index.js
exports.handler = async (event, context) => {
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: "Hi there Tacos",
      event
    })
  }
}

We’ll also need to create a netlify.toml file at the root of the project and let it know which directory to find the function in, which in this case, is “functions.”

// netlify.toml
[build]
  functions = "functions"

If we push to master and go into the dashboard, you can see it pick up the function!

netlify function in the dashboard

If you look at the endpoint listed above it’s stored here:
https://ecommerce-netlify.netlify.com/.netlify/functions/index

Really, for any site you give it, the URL will follow this pattern:
https:/<yoursiteurlhere>/.netlify/functions/<functionname>

When we hit that endpoint, it provides us with the message we passed in, as well as all the event data we logged as well:

the function event in the browser

I love how few steps that is! This small bit of code gives us infinite power and capabilities for rich, dynamic functionality on our sites.

Hook up Stripe

Pairing with Stripe is extremely fun because it’s easy to use, sophisticated, has great docs, and works well with serverless functions. I have other tutorials where I used Stripe because I enjoy using their service so much.

Here’s a bird’s eye view of the app we’ll be building:

First we’ll go to the Stripe dashboard and get our keys. For anyone totally scandalized right now, it’s OK, these are testing keys. You can use them, too, but you’ll learn more if you set them up on your own. (It’s two clicks and I promise it’s not hard to follow along from here.)

testing keys in the stripe dashboard

We’ll install a package called dotenv which will help us store our key and test it locally. Then, we’ll store our Stripe secret key to a Stripe variable. (You can call it anything, but here I’ve called it STRIPE_SECRET_KEY, and that’s pretty much industry standard.)

yarn add dotenv
require("dotenv").config()

const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY)

In the Netlify dashboard, we’ll go to “Build & deploy,” then “Environment” to add in Environment variables, where the STRIPE_SECRET_KEY is key, and the value will be the key that starts with sk.

We’ll also add in some headers so we don’t run into CORS issues. We’ll use these headers throughout the function we’re going to build.

const headers = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "Content-Type"
}

So, now we’ll create the functionality for talking to Stripe. We’ll make sure we’ll handle the cases that it’s not the HTTP method we’re expecting, and also that we’re getting the information we expect.

You can already see here, what data we’re going to be needing to send to stripe by what we check for. We’ll need the token, the total amount, and an idempotency key.

If you’re unfamiliar with idempotency keys, they are unique values that are generated by a client and sent to an API along with a request in case the connection is disrupted. If the server receives a call that it realizes is a duplicate, it ignores the request and responds with a successful status code. Oh, and it’s an impossible word to pronounce.

exports.handler = async (event, context) => {
  if (!event.body || event.httpMethod !== "POST") {
    return {
      statusCode: 400,
      headers,
      body: JSON.stringify({
        status: "invalid http method"
      })
    }
  }

  const data = JSON.parse(event.body)

  if (!data.stripeToken || !data.stripeAmt || !data.stripeIdempotency) {
    console.error("Required information is missing.")

    return {
      statusCode: 400,
      headers,
      body: JSON.stringify({
        status: "missing information"
      })
    }
  }

Now, we’ll kick off the Stripe payment processing! We’ll create a Stripe customer using the email and token, do a little logging, and then create the Stripe charge. We’ll specify the currency, amount, email, customer ID, and give a description while we’re at it. Finally, we’ll provide the idempotency key (pronounced eye-dem-po-ten-see), and log that it was successful.

(While it’s not shown here, we’ll also do some error handling.)

// stripe payment processing begins here
try {
  await stripe.customers
    .create({
      email: data.stripeEmail,
      source: data.stripeToken
    })
    .then(customer => {
      console.log(
        `starting the charges, amt: ${data.stripeAmt}, email: ${data.stripeEmail}`
      )
      return stripe.charges
        .create(
          {
            currency: "usd",
            amount: data.stripeAmt,
            receipt_email: data.stripeEmail,
            customer: customer.id,
            description: "Sample Charge"
          },
          {
            idempotency_key: data.stripeIdempotency
          }
        )
        .then(result => {
          console.log(`Charge created: ${result}`)
        })
    })

Hook it up to Nuxt

If we look back at our application, you can see we have pages and components that live inside the pages. The Vuex store is like the brain of our application. It will hold the state of the app, and that’s what will communicate with Stripe. However, we still need to communicate with our user via the client. We’ll collect the credit card data in a component called AppCard.vue that will live on the cart page.

First, we’ll install a package called vue-stripe-elements-plus, that gives us some Stripe form elements that allow us to collect credit card data, and even sets us up with a pay method that allows us to create tokens for stripe payment processing. We’ll also add a library called uuid that will allow us to generate unique keys, which we’ll use for the idempotency key.

yarn add vue-stripe-elements-plus uuid

The default setup they give us to work with vue-stripe-elements-plus looks like this:

<template>
  <div id='app'>
    <h1>Please give us your payment details:</h1>
    <card class='stripe-card'
      :class='{ complete }'
      stripe='pk_test_XXXXXXXXXXXXXXXXXXXXXXXX'
      :options='stripeOptions'
      @change='complete = $event.complete'
    />
    <button class='pay-with-stripe' @click='pay' :disabled='!complete'>Pay with credit card</button>
  </div>
</template>
<script>
import { stripeKey, stripeOptions } from './stripeConfig.json'
import { Card, createToken } from 'vue-stripe-elements-plus'

export default {
  data () {
    return {
      complete: false,
      stripeOptions: {
        // see https://stripe.com/docs/stripe.js#element-options for details
      }
    }
  },

  components: { Card },

  methods: {
    pay () {
      // createToken returns a Promise which resolves in a result object with
      // either a token or an error key.
      // See https://stripe.com/docs/api#tokens for the token object.
      // See https://stripe.com/docs/api#errors for the error object.
      // More general https://stripe.com/docs/stripe.js#stripe-create-token.
      createToken().then(data => console.log(data.token))
    }
  }
}
</script>

So here’s what we’re going to do. We’re going to update the form to store the customer email, and update the pay method to send that and the token or error key to the Vuex store. We’ll dispatch an action to do so.

data() {
    return {
      ...
      stripeEmail: ""
    };
  },
  methods: {
    pay() {
      createToken().then(data => {
        const stripeData = { data, stripeEmail: this.stripeEmail };
        this.$store.dispatch("postStripeFunction", stripeData);
      });
    },
 ...

That postStripeFunction action we dispatched looks like this:

// Vuex store
export const actions = {
  async postStripeFunction({ getters, commit }, payload) {
    commit("updateCartUI", "loading")

    try {
      await axios
        .post(
          "https://ecommerce-netlify.netlify.com/.netlify/functions/index",
          {
            stripeEmail: payload.stripeEmail,
            stripeAmt: Math.floor(getters.cartTotal * 100), //it expects the price in cents                        
            stripeToken: "tok_visa", //testing token, later we would use payload.data.token
            stripeIdempotency: uuidv1() //we use this library to create a unique id
          },
          {
            headers: {
              "Content-Type": "application/json"
            }
          }
        )
        .then(res => {
          if (res.status === 200) {
            commit("updateCartUI", "success")
            setTimeout(() => commit("clearCart"), 3000)
            …

We’re going to update the UI state to loading while we’re processing. Then we’ll use axios to post to our function endpoint (the URL you saw earlier in the post when we set up our function). We’ll send over the email, the amt, the token and the unique key that we built the function to expect.

Then if it was successful, we’ll update the UI state to reflect that.

One last note I’ll give is that I store the UI state in a string, rather than a boolean. I usually start it with something like “idle” and, in this case, I’ll also have “loading,” “success,” and “failure.” I don’t use boolean states because I’ve rarely encountered a situation where UI state only has two states. When you work with booleans for this purpose, you tend to need to break it out into more and more states, and checking for all of them can get increasingly complicated.

As it stands, I can reflect changes in the UI on the cart page with legible conditionals, like this:

<section v-if="cartUIStatus === 'idle'">
  <app-cart-display />
</section>

<section v-else-if="cartUIStatus === 'loading'" class="loader">
  <app-loader />
</section>

<section v-else-if="cartUIStatus === 'success'" class="success">
  <h2>Success!</h2>
  <p>Thank you for your purchase. You'll be receiving your items in 4 business days.</p>
  <p>Forgot something?</p>

  <button class="pay-with-stripe">
    <nuxt-link exact to="/">Back to Home</nuxt-link>
  </button>
</section>

<section v-else-if="cartUIStatus === 'failure'">
  <p>Oops, something went wrong. Redirecting you to your cart to try again.</p>
</section>

And there you have it! We’re all set up and running to accept payments with stripe on a Nuxt, Vue site with a Netlify function, and it wasn’t even that complicated to set up!

Gatsby Applications

We used Nuxt in this instance but if you wanted to set up the same kind of functionality with something that uses React such as Gatsby, there’s a plugin for that. (Everything is plugin in Gatsby. ☺️)

You would install it with this command:

yarn add gatsby-plugin-netlify-functions

…and add the plugin to your application like this:

plugins: [
  {
    resolve: `gatsby-plugin-netlify-functions`,
    options: {
      functionsSrc: `${__dirname}/src/functions`,
      functionsOutput: `${__dirname}/functions`,
    },
  },
]

The serverless function used in this demo is straight up JavaScript, so it’s also portable to React applications. There’s a plugin to add the Stripe script to your Gatsby app (again, everything is a plugin). Fair warning: this adds the script to every page on the site. To collect the credit card information on the client, you would use React Stripe Elements, which is similar to the Vue one we used above.

Just make sure that you’re collecting from the client and passing all the information the function is expecting:

  • The user email
  • The total amount, in cents
  • The token
  • The idempotency key

With such a low barrier to entry, you can see how you can make really dynamic experiences with JAMstack applications. It’s amazing how much you can accomplish without any maintenance costs from servers. Stripe and Netlify Functions make setting up payment processing in a static application such a smooth developer experience!