Bundling Strapi & Nuxt: E-Commerce Tutorial with Snipcart

In a rush? Skip to technical tutorial or live demo. Also, the Strapi team built a Nuxt e-commerce starter out of our demo, you can find it here.

It feels like we haven’t explored a headless CMS for ages on the blog.

Strapi sounds like an excellent choice to come back to it 👌

It was already a solid product when we first wrote about it back in 2018. However, the team at Strapi didn’t stop the great work there. I can’t wait to see what it has to offer as of today.

In this tutorial, I’ll mix it up with two other powerful tools from the Jamstack ecosystem. The result? A Strapi-powered e-commerce website.

The frontend will be built using the neat Vue framework that is Nuxt.

The e-commerce functionalities will be provided by Snipcart, our JS/HTML shopping cart.

Here are the specific steps:

  • Generating a Strapi app & creating products
  • Consuming products with Nuxt.js
  • Adding e-commerce functionalities with Snipcart
  • Handling custom fields in the cart
  • Integrating a GraphQL plugin
  • Deploying the Strapi & Nuxt e-commerce app

Let’s start by meeting our players.

What’s up with Strapi?

strapi-tutorial

Strapi is an open-source Node.js-based headless CMS. It’s 100% JavaScript, fully-customizable and developer-first.

It has made its way to the top of the most popular headless CMSs thanks to the flexibility it gives to devs to work with any tools they love.

Using Vue, React, or plain JS on the frontend? It doesn’t matter. Consuming the API with REST or GraphQL? Both options work seamlessly. Connecting PostgreSQL, MongoDB, SQLite, MySQL as database? All the same to Strapi.

In our last Strapi post, we highlighted another huge benefit a Node.js headless CMS brings: building apps that are entirely JavaScript—on the frontend and the backend alike. Welcome code universality, performance & easy npm install at every level!

This tutorial will showcase both the flexibility of Strapi and the power of Universal JS apps.

On another note, we’re big fans of the whole redesign the Strapi team did for their branding and marketing website lately. Looking sharp!

Why use Snipcart on a Strapi-e-commerce site?

Even though they’re two completely different products, I could reuse the Strapi definition above to describe Snipcart. I mean, our shopping cart is 100% HTML/JS, fully-customizable and developer-first.

You can already see why it’s a good fit.

Our mission is pretty much the same: not getting in the way of developers to work with the tools they love. The same way you’ll use Strapi only for what a CMS is good at (content management & admin tasks), Snipcart only manages the e-commerce functionalities on your website. That’s it.

Not convinced yet? The tutorial below will show how the shopping cart integration is a breeze.

And what about Nuxt?

nuxt-js-tutorial

I think I’ve made it clear that you could use whatever you want on the frontend when working with Strapi. So why Nuxt.js here?

First, we love Vue.js. Incredible to see the place this independent framework continues to take in the JS ecosystem.

Second, Nuxt does it all. I mean, it’s way more than a static site generator. You can use it to build server-rendered apps, SPAs (single-page apps), or PWAs (progressive web apps).

Nuxt's goal is to make web development powerful and performant with a great developer experience in mind.”

Once again, it fits the grand vision we’re trying to put forward here.

Plus, at the moment of writing, Nuxt reached 1 million monthly downloads on NPM. Quite insane.

Nuxt creator on Twitter

Nuxt creator on Twitter

Okay, enough talking. Let’s get to the nitty-gritty.

Strapi & Nuxt e-commerce tutorial using Snipcart

strapi-nuxt-ecommerce

1. Generating a Strapi app

Use the following command to scaffold a Strapi app:

yarn create strapi-app my_project --quickstart

It’ll install Strapi’s dependencies and start the project locally. You can then jump right into designing content types.

Once the install is finished, you’ll be prompt a new web page where you can create your admin user. Complete this and you’ll be redirected to the dashboard.

strapi-nuxt-tutorial-1

strapi-nuxt-tutorial-2

2. Creating products

2.1 Build a new collection type

First thing to do is to create a new collection type to support the products’ entry. Do so by clicking on the Content-Types Builder tab on the left panel.

strapi-nuxt-tutorial-3

Then, you can click on Continue. The forms are well thought; it should be easy to add the following fields for your Product collection type:

  • title (Text),
  • description (Text),
  • price (Number, float),
  • image (Media, single image).

strapi-nuxt-tutorial-4

strapi-nuxt-tutorial-5

Once it’s done, it should look like this (without the custom repeatable component):

strapi-products

Note: I defined image as Text because I’ll use Heroku for hosting, which has an ephemeral file system. Thus, I’ll upload images somewhere else for this demo, but you wouldn’t have to do this in a proper production setup.

Notice the custom - Component (repeatable) field? This is to support custom fields for products. To create it, click on add another field to this collection type button and choose the component item at the bottom left of the form.

strapi-nuxt-tutorial-6

Call the component Custom_field and create a new Custom category. You can select an icon to your liking.

strapi-nuxt-tutorial-7

Click the Configure component button and set the type as Repeatable component. Hit Add first field to the component.

strapi-nuxt-tutorial-8

Now let’s add fields the same way you did for products, but for your new custom field. This time, create a:

  • title (Text),
  • required (Boolean),
  • options (Text).

Once it’s done, you can click on the save button in the upper right corner. It’s now time to create actual products.

strapi-nuxt-tutorial-9

2.2 Making products

Click on the new Products section of the left side panel, then click on the Add New Product on the upper right corner.

For this demo, I’ll be creating a little cupcake shop. Here’s what I defined:

strapi-ecommerce-products

Hooray, products are created!

One last thing to do before you can fetch your new products is setting permissions to expose them publicly. To do so, go to Roles & Permissions and click on the Public role. In the Permissions section, you should have a Product subsection. You need to check the find and findone boxes.

strapi-nuxt-tutorial-10

Save again and hop at http://localhost:1337/products. You should see a JSON blob representing your products, that’s what you’ll consume to render your e-commerce store.

3. Consuming products with Nuxt

As mentioned, Nuxt.js will be used to create a static website. To scaffold a project, use the following command:

yarn create nuxt-app your-project-name.

For this demo, Yarn is my package manager, and Tailwind CSS is the UI framework. There are no Nuxt modules, ESlint, or testing framework. Without surprise, I also chose the Universal & Static option for (SSR/SSG) in Nuxt as I want to create a static site.

After installation, you can fire up the project with yarn dev just after opening the project in your favorite IDE.

Open the /pages/index.vue file. This will be the homepage of your store. It’s where you want to show the products with their respective name, image & description, but no buy buttons yet. Users will be able to put items in their cart when they’re on an individual product page, something I’ll create later on.

For now, let’s start by fetching the products on the index component mounting event. Swap the entire script tag for this one:

export default {
  data(){
    return {
      products: []
    }
  },
  created: async function () {
    const res = await fetch('http://localhost:1337/products')
    this.products = await res.json()
  }
}

As for the template, use this:

<template>
    <div>
        <div class="m-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 gap-4">
            <div v-for="p in products" :key="p.id" class="border rounded-lg bg-gray-100 hover:shadow-lg">
            <nuxt-link :to="`/products/${p.id}`">
                <div class="rounded-t-lg bg-white pt-2 pb-2">
                    <img class="crop mx-auto" :src="p.image">
                </div>          
                <div class="pl-4 pr-4 pb-4 pt-4 rounded-lg">
                <h4 class="mt-1 font-semibold text-base leading-tight truncate text-gray-700">{{p.title}}</h4>
                <div class="mt-1 text-sm text-gray-700">{{p.description}}</div>
                </div>
            </nuxt-link>
            </div>
        </div>
    </div>

Now, there’ll also be a single page for each product rendered on the home page. Which means you want a route like this: /products/:id. The parameter inside the route is the information you’ll use to render the respective product.

Nuxt gives you a neat way to create such pages structure without ever touching a router grammatically. You only need to use a proper page structure.

For this use case, simply create a products with another folder inside it named _id. In that final folder, create an index.vue file. Nesting folders this way makes your ID parameter ***.

For a use case where ID isn’t required, you could use a single products folder with a _id.vue file inside it instead.

Open the new index.vue file and paste the following content:

<template>
    <div class="flex justify-center m-6">
        <div v-if="this.product !== null">
            <div class="flex flex-col items-center border rounded-lg bg-gray-100">
                <div class="w-full bg-white rounded-lg flex justify-center">
                    <img :src="product.image" width="375">
                </div>
                <div class="w-full p-5 flex flex-col justify-between">
                    <div>
                        <h4 class="mt-1 font-semibold text-lg leading-tight truncate text-gray-700">{{product.title}}</h4>
                        <div class="mt-1 text-gray-600">{{product.description}}</div>
                    </div>
                    <button 
                        class="snipcart-add-item mt-4 bg-white border border-gray-200 d hover:shadow-lg text-gray-700 font-semibold py-2 px-4 rounded shadow"
                        :data-item-id="product.id"
                        :data-item-price="product.price"
                        :data-item-url="`${storeUrl}${this.$route.fullPath}`"
                        :data-item-description="product.description"
                        :data-item-image="product.image"
                        :data-item-name="product.title"
                        v-bind="customFields">
                        Add to cart
                    </button>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
  data(){
    return {
      product: null,
      storeUrl: process.env.storeUrl
    }
  },
  created: async function () {
    const res = await fetch(`https://strapi-snipcart.herokuapp.com/products/${this.$route.params.id}`)
    this.product = await res.json()
  }
}
</script>
<style>
</style>

The code is pretty self-explanatory. I use some async/await sugar to make the code less nested than the classic .then approach. Once this is done, you can test that your products are indeed loaded by firing up a local Nuxt development server using the yarn dev.

4. Adding e-commerce functionalities with Snipcart

Now that you have access to the proper information on your pages, you’ll be able to use it with Snipcart.

First things first, you need to add Snipcart’s necessary scripts. Since you’re aiming for a static build, you want to add the scripts in the nuxt.config.js file instead of directly in your components.

So open that file and replace the link: […] section with this object instead:

link: [
      { rel: 'preconnect', href: "https://app.snipcart.com" },
      { rel: 'preconnect', href: "https://cdn.snipcart.com" },
      { rel: 'stylesheet', href: "https://cdn.snipcart.com/themes/v3.0.16/default/snipcart.css" },
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ],
    script: [
      { src: 'https://cdn.snipcart.com/themes/v3.0.16/default/snipcart.js'} 
    ]

This will inject the stylesheets/script directly in the head of your pages. Now, you only need to add a hidden div to put Snipcart’s API key.

Put it in the layouts/default.vue component as such:

<template>
  <div class="md:flex flex-col w-screen h-screen content-center p-6">
    <NavBar />
    <Nuxt />
    <div hidden id="snipcart" data-api-key="YjdiNWIyOTUtZTIyMy00MWMwLTkwNDUtMzI1M2M2NTgxYjE0"></div>
  </div>
</template>

5. Handling custom fields

At the moment, the custom fields we declared in the products aren’t handled in the cart. It’s not much, but there’s still a little more logic to add, so I want to explain it clearly.

Hop back in the /products/_id/index.vue to change the button section of the template section with:

<button 
  class="snipcart-add-item mt-4 bg-white hover:bg-gray-100 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow"
  :data-item-id="product.id"
  :data-item-price="product.price"
  :data-item-url="`${this.$route.fullPath}`"
  :data-item-description="product.description"
  :data-item-image="product.image"
  :data-item-name="product.title"
  v-bind="customFields">
    Add to cart
</button>

The only thing to change is v-bind="customFields". Now, you need to actually define the logic inside the component with:

  computed: {
    customFields(){
      return this.product["Custom_field"]
        .map(({title, required, options}) => ({name: title, required, options}))
        .map((x, index) => Object.entries(x)
          .map(([key, value]) => ({[`data-item-custom${index + 1}-${key.toString().toLowerCase()}`]: value})))
        .reduce((acc, curr) => acc.concat(curr), [])
        .reduce((acc, curr) => ({...acc, ...curr}))
    }
  }

It's a little rough on the eyes at first, but let’s try to understand what you’re doing here.

You need to generate some data-item-custom{number}-{fieldName}: value key for the buy button. From what I know, creating such dynamic template attributes can’t be done with sugar syntax directly in the template tag. You need to wrap your tag in a JS object and let Vue parse it with v-bind.

This is what’s happening here. CustomFields ultimately returns an array of objects as such:

{
  "data-item-custom1-name":"Intensity",
  "data-item-custom1-required":true,
  "data-item-custom1-options":"Vim|Lemonade|Zesty"
}

For each object in the array, Vue generates an attribute with the name of the object key and the object value's value.

6. Polishing our store

At the moment, you have a functional store, but it lacks some basic functionalities. Navigation would be great, and while you’re at it, why not also add a cart summary in the navigation bar.

Since you probably want want these functionalities to be reusable, let’s embed them in a component. To do so, jump in the /components folder and create a navbar.vue file:

<template>
  <div class="flex justify-between ml-6 mr-6 mt-4">
    <nuxt-link to="/">
      <span class="emoji">
        🧁
      </span>
    </nuxt-link>
    <button class="snipcart-checkout flex items-center">
        <Cart />
        <span class="snipcart-total-price ml-3 font-semibold text-sm text-indigo-500"></span>
    </button>
  </div>
</template>
<script>
import Cart from "./icons/cart.vue"
export default {
    components: {
        Cart
    }
}
</script>
<style>
  .emoji {
    font-size: 30px;
  }
</style>

Note that you could also create components for your products listing on the homepage and your single product pages. This would be helpful if you were to reuse the logic on other pages. There’s no value for it in this small demo—it would add a little heaviness. It’s good to keep re-usability in mind, though.

7. Switching to GraphQL

I kept a good ol’ REST API until now, but Strapi makes it easy to change to a GraphQL using a neat plugin. You can skip to the next part if you wish to stick to the REST API. Otherwise, please stick with us a little longer.

Hop back to your Strapi project and run:

yarn strapi install graphql

The default config is running, so no need to do anything else on Strapi’s side.

Then, jump back in the Nuxt project and run:

yarn add --save @nuxtjs/apollo graphql

Now, you’ll need to add a little config in your nuxt.config.js. Add '@nuxtjs/apollo' to the modules array and add this object:

apollo: {  
  clientConfigs: {
    default: {
      httpEndpoint: process.env.BACKEND_URL || "http://localhost:1337/graphql"
    }
  }
},
env: {
  storeUrl: process.env.STORE_URL ||"http://localhost:1337" 
},

Queries will be created directly in your components, but it’s good to know that Strapi offers a fine GraphQL playground at http://localhost:1337/graphql. If you’re not familiar with GraphQL, it gives you a nice way of experimenting with queries.

Create an apollo/queries/product folder to create the queries directly at your project’s root. Start with a products.gql file to fetch all our products.

query Products {
  products {
    id
    title
    image
    description
  }
}

You get only the fields you want without filtering. GraphQL magic at work right here!

Still, in the same folder, let’s create a query for a single product in a file called product.gql:

query Products($id: ID!) {
  product(id: $id) {
    id
    title
    image
    description,
    price,
    Custom_field {
      title
      required,
      options
    }
  }
}

Last thing you need to do is use these queries. Start with products/index.vue component.

Replace the whole script tag with:

<script>
import productsQuery from '../apollo/queries/product/products'
export default {
  data(){
    return {
      products: []
    }
  },
  apollo: {
    products: {
      prefetch: true,
      query: productsQuery
    }
  }
}
</script>

The created hook is simply replaced by an Apollo object that will run your newly imported query.

The structure of the objects is the same, so it’s really a plug-n-play replacement. You don’t have anything else to change in the template.

Do the same thing with the product/_id/index.vue component:

<script>
import productQuery from '../../../apollo/queries/product/product'
export default {
  data(){
    return {
      product: null,
      storeUrl: process.env.storeUrl
    }
  },
  computed: {
    customFields(){
      return this.product["Custom_field"]
        .map(({title, required, options}) => ({name: title, required, options}))
        .map((x, index) => Object.entries(x)
          .map(([key, value]) => ({[`data-item-custom${index + 1}-${key.toString().toLowerCase()}`]: value})))
        .reduce((acc, curr) => acc.concat(curr), [])
        .reduce((acc, curr) => ({...acc, ...curr}))
    }
  },
  apollo: {
    product: {
      prefetch: true,
      query: productQuery,
      variables () {
        return { id: parseInt(this.$route.params.id) }
      }
    }
  }
}
</script>

8. Deploying the Strapi & Nuxt e-commerce app

Now that everything works locally, you’re ready to deploy the infrastructure and ensure everything keeps working in production.

Strapi is initialized by default with a MySQL database, which can’t be hosted on Heroku for free. So I decided to switch to Postgres to enable free hosting on Heroku. It’s quite simple to do by following this Strapi tutorial.

Still, if you do that, you won’t be able to upload media on the free tier, so I decided to simply host the images on Snipcart’s own CMS. You wouldn’t do that in a proper production setup, but you get the point.

Once the backend is deployed, I’ll use Netlify to deploy the frontend.

To do so, create a new little script in your package.json with the following entry in the “scripts” object:

"generate": "nuxt build && nuxt export".

Then, push your repo to your favorite git hosting platform and create a new Netlify site. Define the deploy settings as such:

strapi-deployment-settings

Last thing you need to add is the BACKEND_URL & STORE_URL. This will serve to both fetch the “production” GraphQL server and set the proper store URL for the data-item-url on your individual product pages.

Here’s my config:

strapi-config

And that’s it! Your Strapi-powered e-commerce website is open for business.

Live demo & GitHub repo

strapi-nuxt-ecommerce-demo

See live demo here.

See GitHub repo here.

Find the Strapi e-commerce starter here

Closing thoughts

I truly enjoyed Strapi. For a newcomer like me, the documentation was A+.

The only issue I had was that I renamed a database field with “name” instead of “Name” and it crashed the DB. Although, with some research, I found out that it had to do with a bad ALTER implementation on MySQL side and nothing to do with Strapi. I loved their CLI scaffolding tool; it was really straightforward and hassle-free.

Overall, I spent 3 to 4 hours to build the demo. I lost some time at the end because I had issues deploying on Netlify. I finally figured out that I had to manually add core-js as a dependency, even though it would still work locally without it.

If you want to go further, creating more complex models could be fun. The products in this demo are quite simple without any other relations to other entities. Obviously, there are many other Snipcart features you could leverage, such as multi-currency.


If you've enjoyed this post, please take a second to share it on Twitter. Got comments, questions? Hit the section below!

Suggested posts: