DEV Community

Cover image for Building a Serverless JAMstack ECommerce Store with Gatsby & AWS Amplify
Nader Dabit for AWS

Posted on • Updated on

Building a Serverless JAMstack ECommerce Store with Gatsby & AWS Amplify

In this post, you will learn how to build a full stack serverless JAMstack ECommerce store using Gatsby, AWS Amplify and JAMstack ECommerce.

JAMstack ECommerce

While this post focuses on the specific use case of building an ECommerce application, the services and features I will showcase are the building blocks for most all real-world production applications, so I hope you will find it useful.

Laying the groundwork

For this site, I've chosen to use Gatsby in order to get the benefits of a static site, including better performance, better SEO, and cheaper / easier scalability. Next.js (React) and Nuxt (Vue) are other options that would do the job just as well, but I've gone with Gatsby because of my previous experience with it as well as the robust developer community and documentation available at the time of this writing.

The app we will be building has the following features:

  1. Ability to query inventory from an API
  2. At build time, create navigation based on inventory categories
  3. At build time, create pages for each inventory item and category pages for each nav item along with corresponding views
  4. Shopping cart / checkout
  5. Admin panel for creating / updating inventory
  6. Downloading of images at build time to serve from the public folder vs dynamic fetching

Based on these features, we can assume that the app will have the following requirements from an API / service standpoint:

  1. Authentication (sign up, sign in)
  2. Dynamic group authorization (only Admin users can view and update inventory)
  3. API with create, update, and delete operations
  4. Public API access for querying the API
  5. Private API access so that only Admin users can create / update / delete inventory
  6. Image / asset hosting

To build out these features on both the front and the back end we will be using the Amplify Framework:

  • Amplify CLI for creating and configuring AWS services
  • Amplify Client libraries for interacting with the services
  • Amplify Console to host and view the app and features after they are deployed.

Let’s start building!

To follow along with this tutorial, you need to have an AWS account (sign up here)

Getting started

To get started, clone the Gatsby JAMstack ECommerce starter project that will serve as the base of the application we'll be building:

$ git clone

Next, change into the directory and install the dependencies using npm or yarn:

$ cd jamstack-ecommerce

$ npm install

# or

$ yarn

Next, start the project to get an idea of how the app will look:

$ gatsby develop

When the app loads, you should be able to go to http://localhost:8000/ and see something like this:
JAMstack ECommerce

Great, we're now up and running!

You may be wondering where the inventory is coming from. Starting off, the inventory is hard-coded in the inventory file located at providers/inventory.js.

This is not ideal though because keeping up with everything locally is hard to scale. Instead, we propose to make the inventory dynamic and be able to add and update inventory via and admin panel using some type of content management system.

To do so, we'll need to set up an API. To start, create the Amplify project so we can begin migrating the inventory provider to a real back end provider.

Installing Amplify and initializing an Amplify project

Before you can use Amplify, you'll first need to have or create an AWS Account.

Next, install the Amplify CLI globally from the command line:

$ npm install -g @aws-amplify/cli

If the CLI is installed, you should be able to run the amplify command and see some output and help options.

$ amplify

Now that the CLI is successfully installed, we now need to configure the CLI. To do so, run the configure command:

$ amplify configure

This will walk you through the steps to create and configure AWS user credentials locally. For a guided walkthrough of these configuration steps, check out this video.

Creating the Amplify project

After the CLI has been configured you can create a new Amplify project:

$ amplify init

? Enter a name for the project: jamstack-ecommerce
? Enter a name for the environment dev
? Choose your default editor: <your_preferred_editor>
? Choose the type of app that youre building: javascript
? What javascript framework are you using: react
? Source Directory Path: src
? Distribution Directory Path: public
? Build Command: gatsby build
? Start Command: npm run start
  • When prompted for an AWS profile, choose the profile you created in the configuration step.

After the initialization has been completed, you should now see 2 artifacts created for you in your project directory:

  1. src/aws-exports.js - This file will hold the key value pairs of the resource information for the services created by the CLI.
  2. amplify directory - This will hold the back end code we write for things like GraphQL schemas and serverless functions managed by the AWS services we'll be using.

Now that we have the base project set up, let's also go ahead and install the AWS Amplify client library:

$ npm install aws-amplify

# or

$ yarn add aws-amplify

Creating the back end services

Now we are ready to go and can start creating the services we'll be integrating into the app. Let's first start with authentication.


The authentication setup for this app will need to accomplish the following things:

  1. Enable users to sign up and sign in
  2. Detect Admin users based on a predetermined list of admins and place them in the Admin group once they sign up.

We can do this with a combination of Amazon Cognito (managed authentication service) and AWS Lambda (functions as a service).

We'll create an authentication service that will call (trigger) a Lambda function when someone signs up (post-confirmation). In that function we can determine whether or not they will be allowed Admin access based on their email address.

To create the service, we'll use the Amplify add command:

$ amplify add auth

? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? Yes
? What attributes are required for signing up? Email (keep defaults)
? Do you want to enable any of the following capabilities? Add User to Group
? Enter the name of the group to which users will be added. Admin
? Do you want to edit your add-to-group function now? Y

Now, let's edit the code for the post-confirmation Lambda trigger. In amplify/backend/function/function_name/src/add-to-group.js, use the following code:

// amplify/backend/function/function_name/src/add-to-group.js
const aws = require('aws-sdk');

exports.handler = async (event, context, callback) => {
  const cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' });

  // Here, update the array to include the Admin emails you would like to use
  let adminEmails = [""], isAdmin = false

  if (adminEmails.indexOf( !== -1) {
    isAdmin = true

  if (isAdmin) {
    const groupParams = {
      GroupName: process.env.GROUP, UserPoolId: event.userPoolId,

    const addUserParams = {
      ...groupParams, Username: event.userName,

    try {
      await cognitoidentityserviceprovider.getGroup(groupParams).promise();
    } catch (e) {
      await cognitoidentityserviceprovider.createGroup(groupParams).promise();

    try {
      await cognitoidentityserviceprovider.adminAddUserToGroup(addUserParams).promise();
      callback(null, event);
    } catch (e) {
  } else {
    callback(null, event);

Update the adminEmails array to include the emails you'd like to allow Admin access.

This function will add a user to the Admin group if their email is included in the adminEmails array.


Next, let's create the image storage service using Amazon S3:

$ amplify add storage

? Please select from one of the below mentioned services: Content
? Please provide a friendly name for your resource...: <resource_name>
? Please provide bucket name: <some_unique_bucket_name>
? Who should have access: Auth and guest users
? What kind of access do you want for Authenticated users? create, update, read, delete
? What kind of access do you want for Guest users? read
? Do you want to add a Lambda Trigger for your S3 Bucket? N

API & database

The last thing we need to create is an API and a database to store our data. This API needs to allow both authenticated and unauthenticated access.

Authenticated Admin users should be able to create and update items in the database while unauthenticated access will allow us to query the API at build time to fetch the data needed for the application.

To allow this, we'll create an AWS AppSync GraphQL API & Amazon DynamoDB NoSQL database using the CLI:

$ amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: furnitureapi
? Choose the default authorization type for the API: Amazon Cognito User Pool
? Do you want to configure advanced settings for the GraphQL API: Yes
? Configure additional auth types? Y
? Choose the additional authorization types you want to configure for the API: API Key
? Enter a description for the API key: gatsby
? After how many days from now the API key should expire: 100
? Configure conflict detection? N
? Do you have an annotated GraphQL schema? N
? Do you want a guided schema creation? Y
? What best describes your project: Single object with fields
? Do you want to edit the schema now? Y

This should open the GraphQL schema located at amplify/backend/api/postershop/schema.graphql. Here, update the schema to be the following:

type Product @model
  @auth(rules: [
    { allow: public, operations: [read] },
    { allow: groups, groups: ["Admin"] }
  ]) {
  id: ID!
  categories: [String]!
  price: Float!
  name: String!
  image: String!
  description: String!
  currentInventory: Int!
  brand: String

This GraphQL schema has a few additional directives that you might not see on a traditional schema:

@model - This directive will scaffold out a DynamoDB database, addition CRUD (Create, Read, Update, Delete) & List GraphQL schema operations, and GraphQL resolvers mapping between the operations and the database.

@auth - This directive allows us to set up authorization rules on either a GraphQL type or field.

These directives are part of the GraphQL Transform library of Amplify. To learn more about this library and these directives, check out the documentation here.

In the schema we've created, we want to have two authorization types:

  1. Admin users can perform all operations
  2. Public access to read items

The services should now be configured and can be deployed to AWS. To do so, we can run the push command:

$ amplify push --y

All of the services have now been deployed and we can start integrating them into the the client application!

To view the AWS services that have been created at any time, open the Amplify console with the following command:

$ amplify console

Client integration

Now that the back end services are deployed, the next thing we need to do is configure the Gatsby project to recognize the Amplify project. To do so, open gatsby-browser.js and add the following code:

import Amplify from 'aws-amplify'
import config from './src/aws-exports'

Client authentication

Once the client app is configured, implement authentication for the admin panel. To do so, open src/pages/admin.js and import the Auth class from Amplify:

// src/pages/admin.js
import { Auth } from 'aws-amplify'

Next, modify the signUp, confirmSignUp, signIn, and signOut methods to the following:

signUp = async (form) => {
  const { username, email, password } = form
  // step 1: Sign up a new user
  await Auth.signUp({
    username, password, attributes: { email }
  this.setState({ formState: 'confirmSignUp' })
confirmSignUp = async (form) => {
  const { username, authcode } = form
  // step 2: Use MFA to confirm the new user
  await Auth.confirmSignUp(username, authcode)
  this.setState({ formState: 'signIn' })
signIn = async (form) => {
  const { username, password } = form
  // step 3: Sign in the new user
  await Auth.signIn(username, password)
  // step 4: Check to see if the user is an Admin, if so, show the inventory view.
  const user = await Auth.currentAuthenticatedUser()
  const { signInUserSession: { idToken: { payload }}} = user
  if (payload["cognito:groups"] && payload["cognito:groups"].includes("Admin")) {
    this.setState({ formState: 'signedIn', isAdmin: true })
signOut = async() => {
  // allow users to sign out
  await Auth.signOut()
  this.setState({ formState: 'signUp' })

Authentication is now enabled and users can begin signing up and signing in to view the inventory.

In the bottom right navigation, click on Admins to view the admin panel to sign up and sign in

In this component, we use a few different methods on the Auth class like signUp and signIn. Auth has over 30 different methods for handling user authentication. To learn more, check out the documentation here or the API here.

Next, let's test it out:

$ gatsby develop

You'll notice that when you sign in and refresh the page, the user state is not persisted. We can fix this by checking to see if the user is signed in when the app loads. To do so, update componentDidMount with the following code:

async componentDidMount() {
  const user = await Auth.currentAuthenticatedUser()
  const { signInUserSession: { idToken: { payload }}} = user
  if (payload["cognito:groups"] && payload["cognito:groups"].includes("Admin")) {
    this.setState({ formState: 'signedIn', isAdmin: true })

Client API integration

Now that we have authentication working, let's use the API to create and update data in our app. To do so, we'll be first working with the inventory provider located at src/templates/ViewInventory.js. Here, let's update it to fetch data from our real API.

First, import GraphQL query and the APIs needed from AWS Amplify:

// src/templates/ViewInventory.js
import { API, graphqlOperation } from 'aws-amplify'
import { listProducts } from '../graphql/queries'

Next, update the fetchInventory method to fetch the data from the API:

fetchInventory = async() => {
  const inventoryData = await API.graphql(graphqlOperation(listProducts))
  const { items } =
  console.log("inventory items: ", items)
  this.setState({ inventory: items })

You'll notice that when we run the app and console.log the items coming back from the API, there is an empty array. This is because we have yet to create any real items in our database.

To add the ability to create items, we'll need to make some updates to src/components/formComponents/AddInventory.js.

First, update the imports to add the following:

// src/components/formComponents/AddInventory.js
import { Storage, API, graphqlOperation } from 'aws-amplify'
import { createProduct } from '../../graphql/mutations'
import uuid from 'uuid/v4'

Next, update the onImageChange and addItem methods to the following:

onImageChange = async (e) => {
  const file =[0];
  const fileName = uuid() +
  // save the image in S3 when it's uploaded
  await Storage.put(fileName, file)
  this.setState({ image: fileName  })
addItem = async () => {
  const { name, brand, price, categories, image, description, currentInventory } = this.state
  if (!name || !brand || !price || !categories.length || !description || !currentInventory || !image) return

  // create the item in the database
  const item = { ...this.state, categories: categories.replace(/\s/g, "").split(',') }
  await API.graphql(graphqlOperation(createProduct, { input: item }))

Now, you'll notice that you can create items and when we view the inventory, they show up!

Client Storage integration

One odd thing you'll notice is that the images do not show up in the inventory view. This is because we are attempting to render an image key from S3 that is not yet signed. We can fix this by opening the image component at src/components/image.js and adding image signing from S3.

We will check to see if the image is a locally downloaded image (if the image path includes downloads). If it does not, then we know it is a remote image from S3 and we will fetch the signed URL for the image.

First, import the Storage class from Amplify:

// src/components/image.js
import { Storage } from 'aws-amplify'

Next, update the fetchImage function to this:

async function fetchImage(src, updateSrc) {
  if (!src.includes('downloads')) {
    const image = await Storage.get(src)
  } else { updateSrc(src) }

Now, we should see the images rendered in the list.

We next need to enable the editing and deleting of items. You'll notice that if you edit an item in the Admin view and refresh, the changes do not persist. To fix that, open src/templates/ViewInventory.js and make the following changes.

First, import the updateProduct and deleteProduct mutations:

// src/templates/ViewInventory.js
import { updateProduct, deleteProduct } from '../graphql/mutations'

Next, update the saveItem and deleteItem methods to the following:

saveItem = async index => {
  const inventory = [...this.state.inventory]
  inventory[index] = this.state.currentItem
  await API.graphql(graphqlOperation(updateProduct, { input: this.state.currentItem }))    
  this.setState({ editingIndex: null, inventory })

deleteItem = async index => {
  const id = this.state.inventory[index].id
  const inventory = [...this.state.inventory.slice(0, index), ...this.state.inventory.slice(index + 1)]
  this.setState({ inventory })
  await API.graphql(graphqlOperation(deleteProduct, { input: { id }}))

Now when we save an item, the updates also go to the database!

Build-time API integration

Finally, we need to change the build step to use the new API we've created instead of the hard-coded inventory data we've created. When we run gatsby develop or gatsby build, we will use the public API access to enable the system to query the data from the API and use it for the app.

We also want to include in the build step a way to download the images locally in our project so we are not fetching remote images, instead we are rendering a local copy of the image that we will be downloading and storing in a local downloads directory in the public folder.

For this to work, first create at least 4 items in your inventory from the admin panel.

Next, create downloadImage.js in the utils folder. This function will allow us to download images locally using the file system (fs) module:

// utils/downloadImage.js
import fs from 'fs'
import axios from 'axios'
import path from 'path'

function getImageKey(url) {
  const split = url.split('/')
  const key = split[split.length - 1]
  const keyItems = key.split('?')
  const imageKey = keyItems[0]
  return imageKey

function getPathName(url, pathName = 'downloads') {
  let reqPath = path.join(__dirname, '..')
  let key = getImageKey(url)
  key = key.replace(/%/g, "")
  const rawPath = `${reqPath}/public/${pathName}/${key}`
  return rawPath

async function downloadImage (url) {
  return new Promise(async (resolve, reject) => {
    const path = getPathName(url)
    const writer = fs.createWriteStream(path)
    const response = await axios({
      method: 'GET',
      responseType: 'stream'
    writer.on('finish', resolve)
    writer.on('error', reject)

export default downloadImage 

Now open gatsby-node.esm.js. Add the following imports and statements at the top of the file:

// gatsby-node.esm.js
import config from './src/aws-exports'
import axios from 'axios'
import tag from 'graphql-tag'
import fs from 'fs'
import downloadImage from './utils/downloadImage'
import Amplify, { Storage } from 'aws-amplify'

const graphql = require('graphql')
const { print } = graphql

Next, create a new function called fetchInventory to fetch inventory from our new API and place the function anywhere in gatsby-node.esm.js.

This function will also map over all of the inventory items and download the images locally at build time using the downloadImage function that we created in the previous step:

async function fetchInventory() {
  /* new */
  const listProductsQuery = tag(`
    query listProducts {
      listProducts(limit: 500) {
        items {
  const gqlData = await axios({
    url: config.aws_appsync_graphqlEndpoint,
    method: 'post',
    headers: {
      'x-api-key': config.aws_appsync_apiKey
    data: {
      query: print(listProductsQuery)

  let inventory =

  if (!fs.existsSync(`${__dirname}/public/downloads`)){

  await Promise.all( (item, index) => {
      try {
        const relativeUrl = `../downloads/${item.image}`
        if (!fs.existsSync(`${__dirname}/public/downloads/${item.image}`)) {
          const image = await Storage.get(item.image)
          await downloadImage(image)
        inventory[index].image = relativeUrl
      } catch (err) {
        console.log('error downloading image: ', err)
  return inventory

Finally, in exports.sourceNodes and exports.createPages update the calls to getInventory with the new fetchInventory functions:

/* replace const inventory = await getInventory() with this 👇 */
const inventory = await fetchInventory()

Run the develop command to test it out:

$ gatsby develop

Running a new build will fetch the data from the GraphQL API and create a new navigation based on the updated product categories and also build out a new static version of the site.


At this point, you are up and running with an MVP of a real-world and scalable ECommerce application running on AWS!

From here, you may want to dive deeper on the Amplify documentation to learn more about the APIs and services we’ve used as well as the other APIs that we’ve not yet worked with.

So far we've set up the following features:

You might also be interested in learning about:

Next steps

Here are a few things you can do to continue improving this app.

Hosting - deploy to the Amplify Console

If you host your app in GitHub, BitBucket, GitLab, or AWS CodeCommit, you can easily deploy the entire site to live hosting and add a custom domain in just a few minutes using the Amplify Console. To see a quick video of how to do this with a Gatsby site in less than one minute, check out this video.

Configure server-side logic to process the payments with Stripe. You can do this easily from where we currently are by adding a serverless function and API using Amplify and the API category:

$ amplify add function

$ amplify add api

- Choose REST

If you’d like to see an example of the function code needed to interact with stripe, check out this code snippet.

Also, consider verifying totals by passing in an array of IDs into the function, calculating the total on the server, then comparing the totals to check and make sure they match.

Update inventory items as they are purchased

To keep the inventory up to date, you probably want to decrement the inventory as a purchase is made. To do this, you could send an update request to decrement the database before an order was confirmed.

To make this even more secure, you could use a DynamoDB Transaction to only process the order if there was enough in the inventory and decrement the number of items if there are any in the inventory in a single operation.

To learn more about Amplify, check out these resources:
Awesome AWS Amplify
My YouTube

Follow me on Twitter at dabit3

Top comments (17)

devanghingu profile image
Devang Hingu

well explained..!! but why need to go through jamstack rather then magento, shopify or wordpress? because they already provide great few click installation & configuration. and it's very secure and popular framework.

dabit3 profile image
Nader Dabit

Good question, you can definitely use Shopify as the inventory provider via their API in the step where we create the Amplify inventory provider, the difference there is that you would not be able to have authorization rules on the client where users could sign up as admins and add / update / delete items.

Also, Magento, Wordpress , and shopify when using their site generators are not nearly as fast as JAMstack apps. Check out the live demo here to get an idea of the difference in speed and performance.

devanghingu profile image
Devang Hingu

Yeah, speed is awesome.!!

whiteman_james profile image
James Whiteman

Another nice overview, thanks Nader.
S3 storage is giving me 403 when I try to build, I just gave the bucket the shotgun approach with the following

    "Version": "2012-10-17",
    "Statement": [
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<bucket>/*"
marksteven profile image

Hi Nader,
I am not sure if I am doing something wrong but cannot get past first base.
If I follow your step 1
git clone

I do indeed get jamstack-ecommerce package

But looking in files and the package.json - pasted below
It appears to be a React rather than a Gatsby project?
Have I gone wrong from the start here or the Git moved somehow?
Thanks in advance Mark

"name": "jamstack-ecommerce",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"dependencies": {
"@stripe/react-stripe-js": "^1.1.2",
"@stripe/stripe-js": "^1.11.0",
"autoprefixer": "^10.1.0",
"next": "10.0.4",
"postcss": "^8.2.2",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-icons": "^4.1.0",
"react-toastify": "^6.2.0",
"tailwindcss": "^2.0.2",
"uuid": "^8.3.2"

pablko profile image
Pablo Colqui

I have some problems to login, first I sign up correctly, even I can see the confirmation in AWS Cognito. But when I try to login, nothing happens, not even console show me anything.

Someone has the same problem?

mathiasjose profile image

Hi Nader
Great tutorial - thanks!

My one question is what happens once the order is created

I see in src>pages>checkout.js line 114 that there's a comment to call API.

How would you go about putting all orders in the database in a new orders table and tying it to the user who created the order so that they can see their history for example.

Thanks again for the great tut

osaosemwen profile image
Ose M. Osamudiamen

Very good article, Nader. well descriptive. Thanks.

Please, I have a couple of issues:

  1. How do I control the site from reloading the whole application whenever there is any change in the code.
  2. How do I solve this bug ? It generates when I try loading a picture in the application. an attachment can be seen for the error.

Unhandled Rejection (TypeError): aws_amplify_WEBPACK_IMPORTED_MODULE_9_.API.grapghql is not a function
281283 | categories: categories.replace(/\s/g, "").split(',')
281284 | });
281285 | = 6;

281286 | return aws_amplify_WEBPACK_IMPORTED_MODULE_9["API"].grapghql(Object(aws_amplifyWEBPACK_IMPORTED_MODULE_9["graphqlOperation"])(_graphql_mutationsWEBPACK_IMPORTED_MODULE_10_["createProduct"], {
| ^ 281287 | input: item
281288 | }));
281289 |

the source code error:
Unhandled Rejection (TypeError): aws_amplify_WEBPACK_IMPORTED_MODULE_9_.API.grapghql is not a function
26 | // })
27 | // this.setState({ image: storageUrl })
28 | }

29 | addItem = async () => {
30 | const { name, brand, price, categories, image, description, currentInventory } = this.state
31 | if (!name || !brand || !price || !categories.length || !description || !currentInventory || !image) return
32 | // create the item in the database.

  1. I also realize that when I try to update the code 'fetchImage' function in the file image.js as suggested, the images would not load up. but when I revert to its initial state it loads up.

Sorry I asked 3 questions in one, issue is, I am hurrying up to round up this task so I move on to integration of financial-payment solutions (stripe and others).


andre347 profile image
Andre de Vries

This is awesome! Thanks Nader!

ywroh profile image

Thank you. Based on the information you provided, I am currently creating an ecommerce site using aws, gatsby, etc. I'll show you when it's done!

josedonato profile image
José Donato

this is amazing, awesome job, Nader!

dabit3 profile image
Nader Dabit


josedonato profile image
José Donato

I'm going to do some contributions to the project on github, is there any way i can contact you?

enthusiast_tech profile image

Is it a good practice to use @model, @searchable - basically amplify thing for production apps, isn't it more suited for a POC?

ywroh profile image

Currently in Korea, we are creating an ecommerce site using the information you provided. Thanks for the good info.

webdril_91 profile image
Emeka Michael

Nice tutorial,
I will like to know the probable monthly cost of this set up if the site was launched in production and used for a small size online retail store.

dangquang1020 profile image
Quang Tran

The JAMstack uses Nextjs for now, any update or the other Github project that uses Gatsby?