How to create a Jamstack pet store app using Stripe, Gatsbyjs, and Netlify functions

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Jamstack is a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup. One of the aspects of Jamstack is, it is practically serverless. To put it more clearly, we do not maintain any server-side applications. Rather, sites use existing services (like email, media, payment platform, search, and so on).

Did you know, 70% - 80% of the features that once required a custom back-end can now be done entirely without it? In this article, we will learn to build a Jamstack e-commerce application that includes,

What are we building today?

I love Cats 🐈. We will build a pet store app called Happy Paws for our customers to purchase some adorable Cats. Customers can buy cats by adding their details to the cart 🛒 and then finally checkout by completing the payment process 💳.

Here is a quick glimpse of the app we intend to build(This is my first ever youtube video with voice. 😍)

TL;DR

In case you want to look into the code or try out the demo in advance, please find them here,

Please note, Stripe is NOT available in all countries. Please check if Stripe is available in your country. The Demo setup uses a test Stripe account created from the India region. Hence, it is guaranteed to work when accessed from India, and I hope it works elsewhere. However, that doesn't stop you from following the rest of the tutorial.

Create the Project Structure

We will use a Gatsby starter to create the initial project structure. First, we need to install the Gatsby CLI globally. Open a command prompt and run this command.

npm install -g gatsby-cli

After this, use this command to create a gatsby project structure,

gatsby new happy-paws https://github.com/gatsbyjs/gatsby-starter-default

Once done, you will see a project folder called happy-paws has been created. Try these commands next,

cd happy-paws
gatsby develop

You should be able to access the interface using http://localhost:8000/

Gatsby Default Starter.png

Setup Netlify Functions

To set up netlify functions, stop the gatsby develop command if running. Install the netlify-cli tool to run these functions locally.

npm install -g netlify-cli

Create a file called netlify.toml at the root of the project folder with the following content,

[build]
    functions = "functions"

 [[redirects]]
   from = "/api/*"
   to = "/.netlify/functions/:splat"
   status = 200

The above file will tell the Netlify tool to pick up the functions from the functions folder at the build time. By default, netlify functions will be available as an API and accessible using a URL prefix, /.netlify/functions. This may not be very user friendly. Hence we want to use a redirect URL as, /api/*. It means, a URL like /.netlify/functions/getProducts can now be accessed like, /api/getProducts.

Next, create a folder called functions at the root of the project folder and create a data folder inside it. Create a file called products.json inside the data folder with the following content.

[
    {
      "sku": "001",
      "name": "Brownie",
      "description": "She is adorable, child like. The cover photo is by Dorota Dylka from Unsplash.",
      "image": {
        "url": "https://res.cloudinary.com/atapas/image/upload/v1604912361/cats/dorota-dylka-_VX-6amHgDY-unsplash_th9hg9.jpg",
        "key": "brownie.jpg"
      },
      "amount": 2200,
      "currency": "USD"
    },
    {
      "sku": "002",
      "name": "Flur",
      "description": "Flur is a Queen. The cover photo is by Milada Vigerova from Unsplash.",
      "image": {
        "url": "https://res.cloudinary.com/atapas/image/upload/v1604829841/cats/milada-vigerova-7E9qvMOsZEM-unsplash_etgmbe.jpg",
        "key": "flur.jpg"
      },
      "amount": 2000,
      "currency": "USD"
    }
]

Here we have added information about two pet cats. You can add as many as you want. Each of the cats is a product for us to sell. It contains information like SKU(a unique identifier common for product inventory management), name, description, image, amount, and the currency.

Next, create a file called, get-products.js inside the functions folder with the following content,

const products = require('./data/products.json');

exports.handler = async () => {
  return {
    statusCode: 200,
    body: JSON.stringify(products),
  };
};

This is our first Netlify Serverless function. It is importing the products from the products.json file and returning a JSON response. This function will be available as API and accessible using /api/get-products.

Execute these commands from the root of the project to access this function,

netlify login

This will open a browser tab to help you create an account with Netlify and log in using the credentials.

netlify dev

To run netlify locally on port 8888 by default. Now the API will be accessible at http://localhost:8888/api/get-products. Open a browser and try this URL.

localhost_8888_api_get-products.png

The beauty of it is, the gatsby UI is also available on the http://localhost:8888 URL. We will not access the user interface on the 8000 port, and rather we will use the 8888 port to access both the user interface and APIs.

Fetch products into the UI

Let us now fetch these products(cats) into the UI. Use this command from the root of the project folder to install a few dependencies first(you can use the npm install command as well),

yarn add axios dotenv react-feather

Now create a file called, products.js inside src/components with the following content,

import React, { useState, useEffect } from 'react';
import axios from "axios";
import { ShoppingCart } from 'react-feather';
import Image from './image';

import './products.css';

const Products = () => {
    const [products, setProducts] = useState([]);
    const [loaded, setLoaded] = useState(false);
    const [cart, setCart] = useState([]);

    useEffect(() => {
        axios("/api/get-products").then(result => {
            if (result.status !== 200) {
              console.error("Error loading shopnotes");
              console.error(result);
              return;
            }
            setProducts(result.data);
            setLoaded(true);
        });
    }, []);

    const addToCart = sku => {
        // Code to come here
    }

    const buyOne = sku => {
        // Code to come here
    }

    const checkOut = () => {
        // Code to come here
    }  

    return (
        <>
        <div className="cart" onClick={() => checkOut()}>
            <div className="cart-icon">
            <ShoppingCart 
                className="img" 
                size={64} 
                color="#ff8c00" 
            />
            </div>
            <div className="cart-badge">{cart.length}</div>
        </div>

        {
            loaded ? (
                <div className="products">
                    {products.map((product, index) => (
                        <div className="product" key={`${product.sku}-image`}>

                            <Image fileName={product.image.key} 
                                style={{ width: '100%' }} 
                                alt={product.name} />
                            <h2>{product.name}</h2>
                            <p className="description">{product.description}</p>
                            <p className="price">Price: <b>${product.amount}</b></p>
                            <button onClick={() => buyOne(product.sku)}>Buy Now</button>
                            {' '}
                            <button onClick={() => addToCart(product.sku)}>Add to Cart</button> 
                        </div>
                    ))
                    }
                </div>
            ) :
            (
                <h2>Loading...</h2>
            )
        }
        </>
    )
};

export default Products;

Note, we are using the axios library to make an API call to fetch all the products. On fetching all the products, we loop through and add the information like image, description, amount, etc. Please note, we have kept three empty methods. We will add code for them a little later.

Add a file called products.css inside the src/components folder with the following content,

header {
    background: #ff8c00;
    padding: 1rem 2.5vw;
    font-size: 35px;
}

header a {
    color: white;
    font-weight: 800;
    text-decoration: none;
}

main {
    margin: 2rem 2rem 2rem 2rem;
    width: 90vw;
}

.products {
    display: grid;
    gap: 2rem;
    grid-template-columns: repeat(3, 1fr);
    margin-top: 3rem;
}

.product img {
    max-width: 100%;
}

.product button {
    background: #ff8c00;
    border: none;
    border-radius: 0.25rem;
    color: white;
    font-size: 1.25rem;
    font-weight: 800;
    line-height: 1.25rem;
    padding: 0.25rem;
    cursor: pointer;
}

.cart {
    position: absolute;
    display: block;
    width: 48px;
    height: 48px;
    top: 100px;
    right: 40px;
    cursor: pointer;
}

.cart-badge {
    position: absolute;
    top: -11px;
    right: -13px;
    background-color: #FF6600;
    color: #ffffff;
    font-size: 14px;
    font-weight: bold;
    padding: 5px 14px;
    border-radius: 19px;
}

Now, replace the content of the file, index.js with the following content,

import React from "react";
import Layout from "../components/layout";
import SEO from "../components/seo";

import Products from '../components/products';

const IndexPage = () => (
  <Layout>
    <SEO title="Happy Paws" />
    <h1>Hey there 👋</h1>
    <p>Welcome to the Happy Paws cat store. Get a Cat 🐈 and feel awesome.</p>
    <small>
      This is in test mode. That means you can check out using <a href="https://stripe.com/docs/testing#cards" target="_blank" rel="noreferrer">any of the test card numbers.</a>
    </small>
    <Products />
  </Layout>
)

export default IndexPage;

At this stage, start the netlify dev if it is not running already. Access the interface using http://localhost:8888/. You should see the page like this,

Initial_product_info.png

It seems we have some problems with the Cat images. However, all other details of each of the cat products seem to be fine. To fix that, add two cat images of your choice under the src/images folder. The images' names should be the same as the image key mentioned in the functions/data/products.json file. In our case, the names are brownie.jpg and flur.jpg.

Edit the src/components/Image.js file and replace the content with the following,

import React from 'react'
import { graphql, useStaticQuery } from 'gatsby'
import Img from 'gatsby-image';

const Image = ({ fileName, alt, style }) => {
  const { allImageSharp } = useStaticQuery(graphql`
    query {
      allImageSharp {
        nodes {
          fluid(maxWidth: 1600) {
            originalName
            ...GatsbyImageSharpFluid_withWebp
          }
        }
      }
    }
  `)

  const fluid = allImageSharp.nodes.find(n => n.fluid.originalName === fileName)
    .fluid

  return (
    <figure>
      <Img fluid={fluid} alt={alt} style={style} />
    </figure>
  )
}

export default Image;

Here we are using Gatsby’s sharp plugin to prebuilt the images. Now rerun the netlify dev command and access the user interface to see the correct images.

with_cat.png

A few more things, open the src/components/Header.js file and replace the content with this,

import { Link } from "gatsby"
import PropTypes from "prop-types"
import React from "react"

const Header = ({ siteTitle }) => (
  <header>
    <Link to="/">
      {siteTitle}
    </Link>
  </header>  
)

Header.propTypes = {
  siteTitle: PropTypes.string,
}

Header.defaultProps = {
  siteTitle: ``,
}

export default Header

Now the header should look much better like,

header-initial.png

But, we want to change that default header text to something meaningful. Open the file gatsby-config.js and edit the title and description of the siteMetaData object as

  siteMetadata: {
    title: `Happy Paws - Cats love you!`,
    description: `Cat store is the one point solution for your Cat`,
  },

This will restart the Gatsby server. Once the server is up, you should see the header text changed to,

header-final.png

Next, let us do the required set up for the Netlify and Stripe integration.

Setup Stripe

Browse to the functions folder and initialize a node project,

npm init -y

This will create a file called package.json. Install dependencies using the command,

yarn add stripe dotenv

This command will install stripe and dotenv library, which is required to manage the environment variables locally.

Get your Stripe test credentials

  • Log into Stripe at https://dashboard.stripe.com/login
  • Make sure the “Viewing test data” switch is toggled on
  • Click “Developers” in the left-hand menu
  • Click “API keys”.
  • Copy both the publishable key and secret key from the “Standard keys” panel

Stripe_API_Keys.png

Create a file called .env at the root of the project with the following content,

STRIPE_PUBLISHABLE_KEY= YOUR_STRIPE_PUBLISHABLE_KEY STRIPE_SECRET_KEY= YOUR_STRIPE_SECRET_KEY

Note to replace the YOUR_STRIPE_PUBLISHABLE_KEY and YOUR_STRIPE_SECRET_KEY with the actual values got from the Stripe dashboard, respectively.

Create a Checkout Function

Next is to create a checkout function using netlify serverless and stripe. Create a file called create-checkout.js with the following content under the function folder.

require("dotenv").config();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const inventory = require('./data/products.json');

const getSelectedProducts = skus => {
  let selected = [];
  skus.forEach(sku => {
    const found = inventory.find((p) => p.sku === sku);
    if (found) {
      selected.push(found);
    }
  });

  return selected;
}

const getLineItems = products => {
  return products.map(
    obj => ({
      name: obj.name, 
      description: obj.description, 
      images:[obj.image.url], 
      amount: obj.amount, 
      currency: obj.currency,
      quantity: 1
    }));
}

exports.handler = async (event) => {
  const { skus } = JSON.parse(event.body);
  const products = getSelectedProducts(skus);
  const validatedQuantity = 1;
  const lineItems = getLineItems(products);

  console.log(products);
  console.log(lineItems);

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    billing_address_collection: 'auto',
    shipping_address_collection: {
      allowed_countries: ['US', 'CA', 'IN'],
    },
    success_url: `${process.env.URL}/success`,
    cancel_url: process.env.URL,
    line_items: lineItems,
  });

  return {
    statusCode: 200,
    body: JSON.stringify({
      sessionId: session.id,
      publishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
    }),
  };
};

Note here we are expecting a payload with the selected product's SKU information. Upon getting that, we will take out other relevant information of the selected products from the inventory, i.e., products.json file. Next, we create the line item object and pass it to the stripe API for creating a Stripe session. We also specify to delegate to a page called success.html once the payment is successful.

UI Changes for Checkout

The last thing we need to do now is to call the new serverless function from the UI. First, we need to install the stripe library for clients. Execute this command from the root of the project folder,

yarn add @stripe/stripe-js

Create a folder called utils under the src folder. Create a file named stripejs.js under src/utils with the following content,

import { loadStripe } from '@stripe/stripe-js';

let stripePromise;
const getStripe = (publishKey) => {
  if (!stripePromise) {
    stripePromise = loadStripe(publishKey);
  }
  return stripePromise;
}

export default getStripe;

This is to get the stripe instance globally at the client-side using a singleton method. Now open the products.js file under src/components to make the following changes,

Import the getStripe function fromutils/stripejs’,

Time to add code for the functions addToCart, byuOne, and checkOut as we left them empty before.

const addToCart = sku => {
   setCart([...cart, sku]);
}

const buyOne = sku => {
    const skus = [];
    skus.push(sku);
    const payload = {
       skus: skus
    };
    performPurchase(payload);
}

const checkOut = () => {
    console.log('Checking out...');
    const payload = {
       skus: cart
    };
    performPurchase(payload);
    console.log('Check out has been done!');
 }

Last, add the function performPurchase, which will actually make the API call when the Buy Now or Checkout buttons are clicked.

const performPurchase = async payload => {
        const response = await axios.post('/api/create-checkout', payload);
        console.log('response', response);
        const stripe = await getStripe(response.data.publishableKey);

        const { error } = await stripe.redirectToCheckout({
            sessionId: response.data.sessionId,
        });

        if (error) {
            console.error(error);
        }
    }

Now restart netlify dev and open the app in the browser, http://localhost:8888

You can start the purchase by clicking on the Buy Now button or add the products to the cart and click on the cart icon at the top right of the page. Now the stripe session will start, and the payment page will show up,

payment_details.png

Provide the details and click on the Pay button. Please note, you can get the test card information from here. The payment should be successful, and you are supposed to land on a success page as we have configured previously. But we have not created a success page yet. Let’s create one.

Create a file called success.js under the src/pages folder with the following content,

import React from 'react';
import Layout from "../components/layout"
import SEO from "../components/seo"

const Success = () => {

    return (
        <Layout>
            <SEO title="Cat Store - Success" />
            <h1>Yo, Thank You!</h1>
            <img src="https://media.giphy.com/media/b7ubqaIl48xS8/giphy.gif" alt="dancing cat"/>
        </Layout>
    )
}
export default Success;

Complete the payment to see this success page in action after a successful payment,

success.png

Great, we have the Jamstack pet store app running using the Netlify serverless functions, Stripe Payment API, and Gatsby framework. But it is running locally. Let us deploy it using Netlify Hosting to access it publicly.

Deploy and Host on Netlify CDN

First, commit and push all the code to your GitHub repository. Login to your netlify account from the browser and click on the ‘New site from Git’ button. Select the option GitHub from the next page,

netlify_1.png

Search and select your GitHub repository to deploy and host,

netlify_2.png

Finally, provide the build options as shown below and click on the ‘Deploy Site’ button.

netlify_3.png

That’s all, and you should have the site live with the app.

Congratulations 🎉 !!! You have successfully built a Jamstack pet shop application with Netlify Serverless functions, Stripe APIs, Gatsby framework, and deployed it on Netlify CDN.

Before we end...

Thank you for reading this far! Let’s connect. You can @ me on Twitter (@tapasadhikary) with comments, or feel free to follow. Please like/share this article so that it reaches others as well.

Do not forget to check out my previous articles on Jamstack,

Comments (4)

James Peters's photo

Great guide man, thanks for writing it! It works great when you manually add the products through products.json, but I am currently trying to use this is on a website where I am getting the product data from Contentful / graphql. How would I go about adding the products from the graphql query to products.json or the checkout function? Thanks again and sorry if this is a stupid question but I'm fairly new to web development.

Show +1 replies
James Peters's photo

Tapas Adhikary, thanks for the fast reply! I'm loading the products directly from Gatsby graphql by using the gatsby-source-contentful plugin.

So if I understood correctly, I will have to use Apollo Client inside the checkout function to query Contentful and get the inventory?

Tapas Adhikary's photo

James Peters, It depends on where and when do you want to fetch the product list. But you can actually do the same in both components of losing and checkout using the react-appolo library as you are calling it directly from GraphQL.

These steps may be helpful,

  • First define the GraphQL query. Here is an example of fetching tags,
const ALL_TAGS = gql`
  query {
    allTags {
      data {
        _id
        name
        description
      }
    }
  }
`
  • Then use the useQuery hook from the apollo react lib.
const { loading: tagLoading, data: tagData } = useQuery(ALL_TAGS)
  • Now you can use the tagData and that has the fetched data.

Note, the lib has the hooks for both fetching and mutating data,

import { useQuery, useMutation } from "@apollo/react-hooks"