DEV Community

Cover image for JAMstack e-commerce using Apicart, FaunaDB, Gridsome and Netlify
Vladimír Macháček
Vladimír Macháček

Posted on

JAMstack e-commerce using Apicart, FaunaDB, Gridsome and Netlify

JAMstack is becoming more and more popular as an approach for web development. This approach is based on composing functionality using APIs of existing services.

We will use this approach to create an e-commerce platform. For generating our pages we will use Gridsome, the e-commerce part of the application will use the Apicart Store API, products, categories and translations will be stored in the FaunaDB and the application will be deployed on the Netlify.

It may sound complicated, however it is more like completing a puzzle, the resulting code is probably smaller than the one of your single page portfolio website :). The code of this article can be found in the Github repository.

Let’s get started!

Table of content

  • Why FaunaDB, Netlify, Gridsome and Apicart
  • Gridsome installation and configuration
  • FaunaDB initialization
  • Apicart integration
  • Netlify deploy configuration
  • Conclusion

Why FaunaDB, Netlify, Gridsome and Apicart

Our application obviously needs some structure and templates. The reason we chose Gridsome is that it is a modern, minimalistic Vue.js static pages generator that allows us to generate pages seamlessly with almost zero configuration. The app structure is clear and simple and with the combination of pages preloading and dynamic loading the result website loads almost instantly.

Because we need dynamic data and we don’t want to maintain our own database we are going to use FaunaDB. FaunaDB is a consistent serverless document based database that provides us with a GraphQL API by simply importing a schema definition. Besides GraphQL, FaunaDB also has the Fauna Query Language (FQL) for more complex queries, functions and advanced conditional transactions which you can use via their drivers in various programming languages. Such a driver exports FQL functions which look just like Javascript and can be composed using the native language constructs. So if you use Javascript you can use Javascript syntax for writing queries. How cool is that?

The reason why we chose Apicart as an e-commerce layer is because it allows you to add a shopping cart into any website in a minute. There is no need for any backend and it is a perfect choice for JAMstack e-commerce of all kind. You just have to copy a piece of code into your page and that’s it. It’s easy to get started and in case you want to integrate e-commerce into any other platform than website, you can use Apicart GraphQL API that provides the same functionality as the Javascript or PHP SDKs.

The application will be deployed to Netlify because we need to run some build tasks and the deploy is basically composed only from three steps and domain configuration.
Netlify provides the easiest way to build and deploy JAMstack websites right now.

Gridsome installation and configuration

We will install Gridsome using the CLI:

  1. Install Gridsome CLI with: yarn global add @gridsome/cli
  2. Create Gridsome project: gridsome create web

Gridsome will create the default structure of our application. If you later encounter a problem with the core-js dependency, set the Gridsome package version in the package.json to "gridsome": "0.7.12" and run yarn install.
Webpack and Babel configuration
The next step is the configuration of Webpack and Babel in order to compile the code:

  1. Go to the package.json and add the browser list configuration. This tells Babel to transpile our code only for latest and most used browsers
"browserslist": [
   "last 1 version",
   "> 1%",
   "IE 11"
]
Enter fullscreen mode Exit fullscreen mode
  1. Now open the gridsome.config.js and copy the following code into it. Webpack will now resolve js files respectively according to the given order in the mainFields array.
const merge = require('webpack-merge')

module.exports = {
   //...
   configureWebpack(config) {
       return merge({
           resolve: {
               mainFields: ['module', 'main', 'browser']
           }
       }, config)
   }
}
Enter fullscreen mode Exit fullscreen mode

Preparing project structure

Because we are going to work with a lot of async data and we need to run a few processes in order to cache loaded and transformed data, we are going to generate our pages manually after all the necessary data is loaded inside the gridsome.server.js using two Vue.js components. Also, in order to minimize and simplify the code we will put all the necessary configuration into a single file called app.js:

  1. Remove a pages and a templates folders in the src directory
  2. Create a static/category and a static/category/product-images directories
  3. Create a src/assets/js/app.js file

Our application also has some dynamic configuration based on the environment. We will put this configuration into the .env file.

# env
ENV=dev
# FaunaDB
FAUNADB_SECRET=YOUR FAUNA DB SECRET
# DATA PROVIDER
DATA_PROVIDER=http://localhost
# Apicart
GRIDSOME_APICART_ENV=dev
GRIDSOME_APICART_STORE_TOKEN=YOUR STORE TOKEN
Enter fullscreen mode Exit fullscreen mode

FaunaDB initialization

First of all you have to sign up. Then we just need to drop in our GraphQL schema and FaunaDB will generate the collections and indexes for us. Then we’ll add the product and translation documents via the FaunaDB dashboard.

Creating collections

Go to the main page in the FaunaDB administration and then follow the steps below:

  1. Create a new database called test
  2. Create a schema.gql file in the project root directory with the following code
type Translation {
   lang: String!
   translations: String!
}
type Product {
   id: Int!
   categoryKeyPath: String!
   pageUrl: String!
   name: String!
   price: Float!
   taxRate: Float
   description: String
}
type Query {
   allTranslations: [Translation!],
   allProducts: [Product!]
}
Enter fullscreen mode Exit fullscreen mode
  1. In the FaunaDB administration, in the left menu click on the GRAPHQL button. On the GrapQL page, in the top menu click on the import schema button and import the schema.gql file
  2. Now open the collections directory. You will see the Translation and the Product collection.

Adding documents

It is time to add some documents, by now we have a full-blown GraphQL API so we could already use GraphQL mutations to create them in the GraphQL playground. However, it’s even easier to create them directly in the FaunaDB collection UI. We are going to add 4 products with categories key paths that will be used to generate categories tree during the build process. If you want to use more than 4 products, just use the same file structure.

In the FaunaDB dashboard, go to Collections, select the Products Collection. For each of the json documents below, press New Document and copy in the product json. The same goes for the translations json at the end, but these will go into the Translation collection. To do so, click on the collections in the left menu, then select the collection in which you want to add a document and then click on the new document button in the top navigation.

Product 1

{
   "id": "1",
   "name": "Men's Modern green t-shirt",
   "categoryKeyPath": "clothes.new",
   "pageUrl": "/green-t-shirt/",
   "price": 25,
   "taxRate": 21,
   "description": "<p><span style=\"letter-spacing: 0.32px\">Men's green sport T-shirt</span><br></p>"
}
Enter fullscreen mode Exit fullscreen mode

Product 2

{
   "id": "2",
   "name": "Men's Modern red t-shirt",
   "categoryKeyPath": "clothes.new",
   "pageUrl": "/red-t-shirt/",
   "price": 20,
   "taxRate": 21,
   "description": "<p><span style=\"letter-spacing: 0.32px\">Men's red sport T-shirt</span><br></p>"
}
Enter fullscreen mode Exit fullscreen mode

Product 3

{
   "id": "3",
   "name": "Men's Modern blue t-shirt",
   "categoryKeyPath": "clothes.discount",
   "pageUrl": "/discounts/blue-t-shirt/",
   "price": 15,
   "taxRate": 21,
   "description": "<p><span style=\"font-size: 0.875rem letter-spacing: 0.02rem;\">Men's blue sport T-shirt</span><br></p>"
}
Enter fullscreen mode Exit fullscreen mode

Product 4

{
   "id": "4",
   "name": "Men's Modern black t-shirt",
   "categoryKeyPath": "clothes.discount",
   "pageUrl": "/discounts/black-t-shirt/",
   "price": 10,
   "taxRate": 21,
   "description": "<p><span style=\"letter-spacing: 0.32px\">Men's black sport T-shirt</span><br></p>"
}
Enter fullscreen mode Exit fullscreen mode

The last document we will add is a document containing translations for our categories. We could use a plain json as a nested document for the translations but for simplicity, we are going to use string.

{
   "lang": "en",
   "translations": "{\"clothes\":{\"title\":\"The most beautiful clothes\",\"description\":\"The most beautiful t-shirts.\",\"menu\":\"T-shirts\",\"new\":{\"title\":\"New collection\",\"description\":\"T-shirts from our new collection.\",\"menu\":\"New collection\"},\"discount\":{\"title\":\"Discounted t-shirts\",\"description\":\"The most popular T-shirts for half price\",\"menu\":\"Discount\"}}}"
}
Enter fullscreen mode Exit fullscreen mode

This is how the collections page looks when you add some documents.
FaunaDB collections page

When you finish adding documents, download the product images, copy them into the static/category/product-images directory and rename them starting by 1 to 4.png (the images are resolved according to the product id).

Black t-shirt
Blue t-shirt
Red t-shirt
Green t-shirt

Generating security token

Before we start communication with the database, we need to create an authorization token first. In the left menu, click on the SECURITY, create a new SERVER token and copy it into the previously created .env file.
FaunaDB administration - tokens

Apicart integration

To start using Apicart you have to sign up first. Then go to the administration, in the left menu, select domains overview, then select your domain and on the domain page open the tab with tokens.
Now Copy the PUBLIC TOKEN for the Store and paste it into your .env file. Next you need to add delivery methods and payment methods. The last thing you have to configure is allowed data providers for product files. Just add the domain where are your products stored
Apicart administration - Provider urls

When the administration is prepared we need to install the Apicart dependencies.
Run yarn add vue-i18n faunadb @apicart/js-utils @apicart/core-sdk @apicart/payments-sdk @apicart/store-sdk @apicart/vue-components @apicart/web-components-localization.

Layout component

Now copy the following pieces of code into the src/layouts/Default.vue file. The template of the layout is composed from the cart dropdown, checkout dialog, cart dialog and order dialog component.

<template>
   <div class="layout">
       <div class="header__wrapper">
           <header class="container header">
               <div class="header__brand">
                   <g-image class="header__brand-image" width="50"  src="~/assets/images/apicart-logo.png" alt="" />
                   <h1 class="header__brand-title">JAMstack E-commerce</h1>
               </div>
               <apicart-cart-dropdown @toggle-button-click="openCheckout" @footer-button-click="openCheckout" />
           </header>
       </div>
       <main class="container">
           <slot/>
       </main>
       <apicart-checkout-dialog ref="apicartCheckoutDialog" />
       <apicart-cart-dialog ref="apicartCartDialog" />
       <apicart-order-dialog ref="apicartOrderDialog" />
   </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The script that is in the layout template initializes mentioned components. Also there is a handler for opening the checkout dialog when clicking on the cart dropdown footer button.

<script>
import { ApicartCartDropdown, ApicartCheckoutDialog, ApicartCartDialog, ApicartOrderDialog } from '~/assets/js/app.js';

export default {
   components: {
       'apicart-cart-dropdown': ApicartCartDropdown,
       'apicart-cart-dialog': ApicartCartDialog,
       'apicart-checkout-dialog': ApicartCheckoutDialog,
       'apicart-order-dialog': ApicartOrderDialog
   },
   methods: {
       openCheckout() {
           this.$refs.apicartCheckoutDialog.open();
       }
   }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The stylesheet in this component is a small common style for the whole website.

<style>
body, html {
   margin: 0;
   padding: 0;
}
* {
   box-sizing: border-box;
}
body {
   font-family: Arial, Helvetica, sans-serif;
}
.container {
   max-width: 1240px;
   margin: 0 auto;
}
.header {
   display: flex;
   align-items: center;
   justify-content: space-between;
}
.header__brand {
   display: flex;
   align-items: center;
}
.header__brand-image {
   width: 50px;
   margin-right: 10px;
}
.header__brand-title {
   margin: 0;
   font-weight: normal;
}
.header__wrapper {
   border-bottom: 1px solid #eee;
   padding: 20px 10px
}
</style>
Enter fullscreen mode Exit fullscreen mode

Homepage component

Create a src/components/Homepage.vue file for the homepage content and add the following pieces of code into it.

Inside this template we use the Apicart category component to generate products list and the categories list based on the configuration in the src/assets/js/app.js shown later in this article.

<template>
 <Layout>
       <apicart-category class="category" />
 </Layout>
</template>
Enter fullscreen mode Exit fullscreen mode

The script in this component only initializes the Apicart category component and sets page meta tags.

<script>
import { ApicartCategory } from '~/assets/js/app.js';

export default {
   components: {
       'apicart-category': ApicartCategory,
   },
   metaInfo: {
       title: 'Category',
       meta: [
           { key: 'description', name: 'description', content: 'Amazing e-commerce!' }
       ]
   }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Product detail component

Create a src/components/ProductDetail.vue file and again copy the following pieces of code into it.

This file contains a link on the homepage and the Apicart product detail component into which we pass preloaded data from the database and a product url in order to render the product detail.

<template>
 <Layout>
     <p class="breadcrumb">
       <g-link to="/" class="breadcrumb__link">
           <strong class="breadcrumb__icon apicart-icon-arrow-left-circle"></strong>
           <strong>Back to category</strong>
       </g-link>
     </p>
     <div>
       <apicart-product-detail :productUrl="$context.productUrl" :productData="$context.productData" />
     </div>
 </Layout>
</template>
Enter fullscreen mode Exit fullscreen mode

Beside the Apicart product detail component initialization the script also sets page headers and meta tags.

<script>
import { ApicartProductDetail } from '~/assets/js/app.js';

export default {
   components: {
       'apicart-product-detail': ApicartProductDetail
   },
   metaInfo() {
       return {
           title: this.$context.productData.name,
           meta: [
               { key: 'description', name: 'description', content: this.$context.productData.description }
           ]
       }
   }
}
</script>
Enter fullscreen mode Exit fullscreen mode

On the end of the file, there is a short block with a few lines of css for the breadcrumb.

<style>
.breadcrumb {
   margin: 24px 0;
}
.breadcrumb__icon {
   margin-right: 4px;
}
.breadcrumb__link {
   display: flex;
   padding-bottom: 2px;
   align-items: center;
   color: inherit;
   text-decoration: none;
   border-bottom: 1px solid;
   display: inline-block;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Preloading data

Our products and categories are preloaded from FaunaDB. We will use their Javascript driver for it. Copy the following code into the
gridsome.server.js.

const fs = require('fs');
const faunadb = require('faunadb');
const q = faunadb.query;
// Create FaunaDB client with the token from the .env file
const client = new faunadb.Client({ secret: process.env.FAUNADB_SECRET });
const Utils = require('@apicart/js-utils');

module.exports = function (api) {
   // If the app runs on the dev environment, set Webpack mode to development
   if (process.env.ENV === 'dev') {
       api.chainWebpack(config => {
           config.mode('development')
       });
   }

   api.createManagedPages(async ({ createPage }) => {
       // 1. Preload data from the Fauna Database
       const getTranslations = async () => {
           const response = await client.query(
               q.Map(
                   q.Paginate(q.Match(q.Index('allTranslations'))),
                   q.Lambda(x => q.Get(x))
               )
           );
           const translations = {};
           response.data.forEach((translation) => {
               const translationData = translation.data;
               Utils.Objects.assign(
                   translations, translationData.lang + '.categories', JSON.parse(translationData.translations)
               );
           });
           return translations;
       };

       const getProducts = async () => {
           const response = await client.query(
               q.Map(
                   q.Paginate(q.Match(q.Index('allProducts'))),
                   item => q.Get(item)
               )
           );

           const products = [];
           response.data.forEach((product) => {
               const dataProvider = process.env.DATA_PROVIDER;
               const productData = product.data;
               Utils.Objects.assign(
                   productData,
                   'images.primary.url', dataProvider + '/category/product-images/' + productData.id + '.png'
               );
               products.push({
                   dataUrl: process.env.DATA_PROVIDER + '/category/product-' + productData.id + '.json',
                   data: productData,
                   pageUrl: productData.pageUrl
               })
           });

           return products;
       };

       const getCategoryProducts = (products) => {
           const categories = {};
           products.forEach((product) => {
               const productData = product.data;
               let category = Utils.Objects.find(categories, productData.categoryKeyPath);

               if (!category) {
                   category = [];
               }
               category.push({
                   dataUrl: process.env.DATA_PROVIDER + '/category/product-' + productData.id + '.json',
                   data: productData,
                   pageUrl: productData.pageUrl
               });
               Utils.Objects.assign(categories, productData.categoryKeyPath, category);
           });

           return categories;
       };

       const products = await getProducts();
       const categoryProducts = getCategoryProducts(products);
       const translations = await getTranslations();

       // 2. Save preloaded data into data files
       fs.writeFileSync("./static/category/products.json", JSON.stringify(categoryProducts));
       fs.writeFileSync("./static/category/translations.json", JSON.stringify(translations));

       products.forEach((product) => {
           fs.writeFileSync('./static/category/product-' + product.data.id +'.json', JSON.stringify(product.data));
       });

       // 3. Create the homepage
       createPage({
           path: '/',
           component: './src/components/Homepage.vue'
       });

       // 4. Create product pages
       const productPages = [];
       products.forEach((product) => {
           productPages.push({
               path: product.pageUrl,
               component: './src/components/ProductDetail.vue',
               context: {
                   productUrl: product.dataUrl,
                   productData: product.data
               }
           })
       });

       productPages.forEach((pageConfig) => {
           createPage(pageConfig);
       });
   });
}
Enter fullscreen mode Exit fullscreen mode

Let’s explain the code above. First of all we load all the dependencies we need. Next, if the application runs on the dev environment, we switch the Webpack mode to development.

Then we load data from FaunaDB and transform them into appropriate structures. Each product is transformed into the following structure:

{
   dataUrl: 'http://producturl.com', // Product data file url
   data: {}, // Data from the database
   pageUrl: {} // Page url that is used in the products list
}
Enter fullscreen mode Exit fullscreen mode

Products categoryKeyPaths from the database used in the Utils.Objects.assign will create the following structure:

{
   clothes: {
       new: [],
       discount: []
   }
}
Enter fullscreen mode Exit fullscreen mode

Apicart components and SDK configuration

Create src/assets/js/app.js file and copy the following code into it:

import Vue from 'vue';
import VueI18n from 'vue-i18n';
export * from '@apicart/vue-components/lib';
import {
   ApicartSkeletonBundleSdk,
   ApicartProductDetail,
   ApicartCartDropdown,
   ApicartCategory,
   ApicartCheckoutDialog,
   ApicartCartDialog,
   ApicartOrderDialog
} from '@apicart/vue-components/lib';
import categoriesTranslations from '../../../static/category/translations.json';
import categoriesProducts from '../../../static/category/products.json';

// Use Vuei18n
Vue.use(VueI18n);

// Set to https://store-api.apicart.dev if on dev environment
if (process.env.GRIDSOME_APICART_ENV === 'dev') {
   ApicartSkeletonBundleSdk.setDevEnv();
}

// Configure Apicart
ApicartSkeletonBundleSdk.configure({
   store: new ApicartSkeletonBundleSdk.Store({ token: process.env.GRIDSOME_APICART_STORE_TOKEN }),
   vueComponents: {
       cartDropdown: {
           dropdownPosition: 'right'
       },
       category: {
           products: {
               list: categoriesProducts
           }
       }
   },
   vueComponentsTranslator: {
       actualLocale: 'en',
       currencyFormats: {
           en: {
               currency: {
                   currency: 'USD'
               }
           }
       },
       localization: categoriesTranslations
   }
});

export {
   ApicartSkeletonBundleSdk,
   ApicartCartDropdown,
   ApicartCategory,
   ApicartCheckoutDialog,
   ApicartCartDialog,
   ApicartOrderDialog,
   ApicartProductDetail
}
Enter fullscreen mode Exit fullscreen mode

What we do here is that we import all our dependencies and preloaded data. Then we setup the VueI18n plugin and if the application runs on the dev environment we switch the Apicart Store API target to the dev domain. Then we just configure translations and categories. At the end of the file, there is an export of all dependencies and components we are going to use.

Netlify deploy configuration

Create an account on the Netlify and sign in. If you have never deployed anything to the Netlify yet you will see the Netlify guider that describes the deploy step by step. But in short, it goes like this:

  1. Sign in to the Git provider
  2. Select the repository you want to deploy
  3. Add the build command and select the publish repository
  4. Deploy your application Netlify deploy

After successful deploy, copy the domain where your application is running and add it in the Apicart administration as allowed data provider (shown above). Otherwise the Apicart will not be able to use product files from your domain.

Conclusion

The result application can be viewed on the https://apicart-fauna-gridsome-example.netlify.app/ url.

If everything went well, then based on the number of products you have added, the default page of your application should looks like this.
Apicart, FaunaDB, Gridsome and Netlify preview

The product detail page should be also available after clicking on any product.
Apicart product detail preview

The page also gets a pretty good score when running Google Chrome audits.
Apicart, FaunaDB, Gridsome, Netlify, JAMstack google chrome audits

As you can see, with Apicart, FaunaDB, Gridsome and Netlify for deploys you almost don’t have to program anything and you can easily create a simple, scalable e-commerce for your customers for almost zero price in a very short time. The code is small, readable, easily maintainable and we didn’t have to configure any servers. The JAMstack approach is just simply awesome!

Top comments (3)

Collapse
 
thetre97 profile image
Travis Reynolds

Thanks - great article! Would be great if you could post a link to this on the Gridsome Discord channel?

Collapse
 
machy8 profile image
Vladimír Macháček

Thanks for the rating and the idea with the Gridsome Discord channel! I will post the article there too :)

Collapse
 
elliotandres profile image
Elliot Andres

Awesome, loved the article.