How to create a Jamstack pet store app using Stripe, Gatsbyjs, and Netlify functions
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: A complete payment platform with rich APIs to integrate with.
- Netlify Serverless Lambda Function: Run serverless lambda functions to create awesome APIs.
- Gatsbyjs: 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. 😍)
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. Don't forget to give it a star if you find it useful.
- Demo
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/
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.
The beauty of it is, the
gatsby
UI is also available on thehttp://localhost:8888
URL. We will not access the user interface on the8000
port, and rather we will use the8888
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,
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.
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,
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,
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
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 from ‘utils/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,
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,
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,
Search and select your GitHub repository to deploy and host,
Finally, provide the build options as shown below and click on the ‘Deploy Site’ button.
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
,