How to create a Jamstack pet store app using Stripe, Gatsbyjs, and Netlify functions
Tags: jamstack, javascript, react, programming
Published: 2021-01-15 05:58:23

`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,

*   [Stripe](https://stripe.com/docs/api): A complete payment platform with rich APIs to integrate with.
*   [Netlify Serverless Lambda Function](https://www.netlify.com/products/functions/): Run serverless lambda functions to create awesome APIs.
*   [Gatsbyjs](https://www.gatsbyjs.com/): A React-based framework for creating prebuilt Markups.

# 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. 😍)

{% youtube TMJx1erg5Us %}

# TL;DR

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

*   [GitHub Repository => Source Code](https://github.com/atapas/catstore). Don't forget to give it a star if you find it useful.
*   [Demo](https://happy-paws.netlify.app/)

> Please note, Stripe is NOT available in all countries. Please check if [Stripe is available in your country](https://stripe.com/global). 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.

```shell
npm install -g gatsby-cli
```

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

```shell
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,

```shell
cd happy-paws
gatsby develop
```

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

![Gatsby Default Starter.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1610350764226/JtAfU1l1j.png?border=1,CCCCCC\&auto=compress)

# 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.

```shell
npm install -g netlify-cli
```

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

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

The above file will tell the [Netlify](https://docs.netlify.com/routing/redirects/redirect-options/#splats) 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.

```js
[
    {
      "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,

```js
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,

```shell
netlify login
```

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

```shell
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610351273712/68uOzRJeH.png?border=1,CCCCCC\&auto=compress)

> 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),

```shell
yarn add axios dotenv react-feather
```

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

```js
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,

```css
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,

```js
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610351678332/apeSl1\_Po.png?border=1,CCCCCC\&auto=compress)

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,

```js
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610351797490/BWYHWah0V.png?border=1,CCCCCC\&auto=compress)

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

```js
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610351868274/vjdf4Zc81.png?border=1,CCCCCC\&auto=compress)

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

```js
  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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610351990137/9cYZ_cqtT.png?border=1,CCCCCC\&auto=compress)

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,

```shell
npm init -y
```

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

```shell
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610352296913/GYVuFsGBS.png?border=1,CCCCCC\&auto=compress)

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.

```js
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,

```shell
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,

```js
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,

```js
Import the getStripe function from ‘utils/stripejs’,
```

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

```js
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.

```js
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610352759467/RnTuAb7tE.png?border=1,CCCCCC\&auto=compress)

Provide the details and click on the Pay button. Please note, [you can get the test card information from here](https://stripe.com/docs/testing#cards). 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,

```js
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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610352879312/RFnap2tXz.png?border=1,CCCCCC\&auto=compress)

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](https://cdn.hashnode.com/res/hashnode/image/upload/v1610352929789/fEkqF1\_1r.png?border=1,CCCCCC\&auto=compress)

Search and select your GitHub repository to deploy and host,

![netlify\_2.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1610352947165/C_NYh4xXN.png?border=1,CCCCCC\&auto=compress)

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

![netlify\_3.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1610352970846/K6sCavqz5.png?border=1,CCCCCC\&auto=compress)

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)](https://twitter.com/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`,

*   [JAMstack for All: An Introduction](https://blog.greenroots.info/jamstack-for-all-an-introduction-cke2fxc5800jvabs15lhn4a9x)
*   [JAMstack vs traditional monolithic workflow](https://blog.greenroots.info/jamstack-vs-traditional-monolithic-workflow-cke6wmr0o00tzxms1dkid7so5)
*   [What is a Static Site Generator and how to select one?](https://blog.greenroots.info/what-is-a-static-site-generator-and-how-to-select-one-cke9xtx5g006p58s11dzg2j16)
*   [Hey, I have made a demo lab using JAMstack](https://blog.greenroots.info/hey-i-have-made-a-demolab-using-jamstack-ckeeiohqp0032f7s15c4c3ro6)

---
# Other articles of mine that you might like
- [3 quick ways to add fonts to your React app](https://blog.greenroots.info/3-quick-ways-to-add-fonts-to-your-react-app)
- [The definitive guide to JavaScript Debugging [2021 Edition]](https://blog.greenroots.info/the-definitive-guide-to-javascript-debugging-2021-edition)
- [Princess Finder using React, ml5.js, and Teachable Machine Learning](https://blog.greenroots.info/princess-finder-using-react-ml5js-and-teachable-machine-learning)
- [How to create React form with a single change event handler?](https://blog.greenroots.info/how-to-create-react-form-with-a-single-change-event-handler)
- [I've completed a HackFest project, here is what I've learned](https://blog.greenroots.info/ive-completed-a-hackfest-project-here-is-what-ive-learned)