JAMstack e-commerce using Apicart, FaunaDB, Gridsome and Netlify
Tags: apicart, fauna, jamstack, vue
Published: 2020-05-26 15:52:06

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](https://gridsome.org), the e-commerce part of the application will use the [Apicart](https://apicart.net) Store API, products, categories and translations will be stored in the [FaunaDB](https://fauna.com) and the application will be deployed on the [Netlify](https://netlify.com). 

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](https://github.com/apicart/article-apicart-gridsome-fauna-netlify).

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](https://gridsome.org/) 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](https://fauna.com/). 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](https://docs.fauna.com/fauna/current/api/fql/))  for more complex queries, functions and advanced conditional transactions which you can use via their [drivers](https://docs.fauna.com/fauna/current/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](https://apicart.net) 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](https://apicart.net/en-us/product/apicart-store/getting-started/installation/) and in case you want to integrate e-commerce into any other platform than website, you can use [Apicart GraphQL API](https://apicart.net/en-us/product/apicart-store/api/introduction/) that provides the same functionality as the Javascript or PHP SDKs. 

The application will be deployed to [Netlify](https://www.netlify.com/) 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

```json
"browserslist": [
   "last 1 version",
   "> 1%",
   "IE 11"
]
```

2. 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.
```js
const merge = require('webpack-merge')
 
module.exports = {
   //...
   configureWebpack(config) {
       return merge({
           resolve: {
               mainFields: ['module', 'main', 'browser']
           }
       }, config)
   }
}
```
### 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
```

## FaunaDB initialization
First of all you have to  [sign up](https://dashboard.fauna.com/accounts/register). 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
```graphql
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!]
}
```
3. 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
4. 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
```json
{
   "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>"
}
```

Product 2
```json
{
   "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>"
}
```

Product 3
```json
{
   "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>"
}
```

Product 4
```json
{
   "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>"
}
```
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.
```json
{
   "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\"}}}"
}
```
This is how the collections page looks when you add some documents.
![FaunaDB collections page](https://dev-to-uploads.s3.amazonaws.com/i/7lc799oe0g6z4dcdxudw.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/zrrfvyol90hpdn1xxgq1.png)
![Blue t-shirt](https://dev-to-uploads.s3.amazonaws.com/i/v62li6r65v5kbfla1iak.png)
![Red t-shirt](https://dev-to-uploads.s3.amazonaws.com/i/a2cn33nrvj8giccz3w77.png)
![Green t-shirt](https://dev-to-uploads.s3.amazonaws.com/i/1o50cii4ogo2apg43vg1.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/i/d5xxlll2jmrjyeovckev.png)

## Apicart integration
To start using Apicart you have to [sign up](https://accounts.apicart.dev/sign-up) first. Then go to the [administration](https://store-admin.apicart.dev), 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](https://apicart.net/en-us/product/apicart-store/getting-started/adding-delivery-methods/) and [payment methods](https://apicart.net/en-us/product/apicart-store/getting-started/adding-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](https://dev-to-uploads.s3.amazonaws.com/i/b0dkazyl7laae67s8h1p.png)

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

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

The stylesheet in this component is a small common style for the whole website.
```vue
<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>
```
### 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.
```vue
<template>
 <Layout>
       <apicart-category class="category" />
 </Layout>
</template>
```
The script in this component only initializes the Apicart category component and sets page meta tags.
```vue
<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>
```
### 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.
```vue
<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>
```
Beside the Apicart product detail component initialization the script also sets page headers and meta tags.
```vue
<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>
```
On the end of the file, there is a short block with a few lines of css for the breadcrumb.
```vue
<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>
```
### 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`.
```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);
       });
   });
}
```
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:
```js
{
   dataUrl: 'http://producturl.com', // Product data file url
   data: {}, // Data from the database
   pageUrl: {} // Page url that is used in the products list
}
```

Products categoryKeyPaths from the database used in the Utils.Objects.assign will create the following structure:
```js
{
   clothes: {
       new: [],
       discount: []
   }
}
```

### Apicart components and SDK configuration
Create `src/assets/js/app.js` file and copy the following code into it:
```js
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
}
```

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](https://app.netlify.com/signup)  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](https://dev-to-uploads.s3.amazonaws.com/i/az19ox7wa76qfakwt7vz.png)

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/](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](https://dev-to-uploads.s3.amazonaws.com/i/titx7onvz75rr8dftmtf.png)

The product detail page should be also available after clicking on any product.
![Apicart product detail preview](https://dev-to-uploads.s3.amazonaws.com/i/5a9v2tw5fhuygx4ggt45.png)

The page also gets a pretty good score when running Google Chrome audits.
![Apicart, FaunaDB, Gridsome, Netlify, JAMstack google chrome audits](https://dev-to-uploads.s3.amazonaws.com/i/8fv837sf70h5phjogroh.png)

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!