Build an Angular E-Commerce App on Top of Sanity's Headless CMS

In a rush? Skip to the tutorial steps

Ever listen to someone order a coffee at Starbucks?

"A tall, half-fat, no-whip, sugar-free, iced mocha latte.”

I used to make fun of those people until I realized: "Wait...if given the options, why not custom build exactly what you want?"

But now the big problem: What do I want?

we-don't-serve-small-black-coffee

JS frameworks and headless CMS are like customized coffees: tons of great choices, but eventually, you just got to pick one.

Personally, I spent too much time researching different tools for this tutorial. But I finally made my decision:

Today I'll show you how to enable Angular e-commerce on top of Sanity, a more-than promising headless CMS. Some store features, like the shopping cart & back office, will be offloaded to Snipcart, our dev-first e-commerce solution.

(And just like my tall, half-fat, no-whip, sugar-free, iced mocha latte, I couldn't be happier with this decision).

More specifically, this comprehensive post will cover:

The result will be a fully decoupled JAMstack e-commerce bundle.

Let's see what we're playing with first, and why.

An update on Angular

angular-overview

If you're reading this, you probably already have a working knowledge of Angular, so there's not much point in beating a dead horse here.

That said, there have definitely been some new developments in the Angular world that are worth a quick mention. Angular's latest 8.0 release boasts loads of bug fixes making it smaller, faster & easier to use. Plus, it now supports TypeScript 3.4. The biggest update, though, is without a doubt Angular's new compiler/runtime, Ivy. Currently Ivy is optional in Angular though it will likely become the default compiler in the future.

Still not 100% clear on Ivy? Check out this this article to learn what Ivy is and its role in Angular

Now, despite the fact that Angular's "Would not use again" score jumped up 24% in 2018 on the State of JS Survey, it's still more popular than Vue.js and more in demand on the job market according to the X-Team's blog post on top JS trends to watch in 2019. At the end of the day, Angular ain't goin' anywhere.

And since I'd never used Angular before crafting this tuts, I was intrigued by its latest release. So I asked myself the following question...

Is Angular any good for e-commerce?

Spoiler Alert:

yup-to-angular-for-ecommerce

And here's a few reasons why:

Great Tooling: Angular has tons of awesome tools like Angular CLI (recently updated), Angular Elements, and, with the latest release, Ivy (like we mentioned above). If you're just getting started with Angular, check out CodingLatte's recommendations for "5 Must Have Tools for Angular Developers.".

The point is that when it comes building your Angular e-commerce app, you'll never be at a lack of intuitive, time-saving tools to get your site up and running as quickly—and painlessly—as possible.

In the world of e-commerce, saving time = saving $.

Loads of Helpful Documentation/Articles: Angular is one of the top JavaScript frameworks in 2019. As such, it should be no surprise that you have loads of resources to pull from for any updated news, troubleshooting problems, or debugging issues you may encounter while building your e-commerce app.

Apart from Angular's killer docs, you have countless other active writers in the Angular community. Take, for example, Juri Strumpflohner's article "Update to Angular Version 8 Now!" which was released just a few days after Angular's v8.0 official release.

It's detailed resources like these (which would be too many to enumerate) that you'll have quite literally at your fingertips as you build your online store..

SEO Opportunities: Look, if you're entering the world of e-commerce, you need to make sure your site is optimized for SEO. Using Angular (or any other JS framework) can be scary when considering SEO purposes, but it certainly doesn't need to be! In fact, with simple tools like Angular's Universal, you can rest assured that your site will be 100% SEO-friendly :)

For more information, you can read this guide to learn how to deal with this issue.

In fact, I'm definitely not the only one who's seen Angular's killer e-commerce potential. Here are some other Angular-based e-commerce apps:

Though the road wasn't entirely unpaved, I was still intrigued enough to build my own. And since I'm a huge fan of the JAMstack, you just know I had to go headless.

But before we dive any deeper, I want to give a quick refresher on what it means to "go headless" to make sure we're all on the same page here.

Simply put, going "headless" means decoupling the backend from the frontend which carries certain advantages for developers including higher flexibility, more freedom for customization, and cheaper/more secure websites. Rather than trying to force a CMS to do everything, a headless e-commerce architecture efficiently separates backend from frontend tasks to keep things from getting too "clunky."

If you're looking for a bit more in-depth guide to going headless, check out our developer's guide to headless e-commerce or listen to this JAMstack talk. Both resources will rock your world, in a good way ;)

Sanity's (awesome) headless CMS

sanity-headless-cms-updated

Sanity is a fully customizable headless CMS. Its two main "parts" are the Content Studio & their query APIs. The former is a React-based, open source, collaborative editor for content managers. The latter is quite interesting: it's their own implementation of a graph-oriented query language (GROQ). It makes connecting to any delivery layer super easy.

We've played with quite a few headless CMS, and like the Netlife team, this one frankly impressed us. From unique query language to real-time collaboration, Sanity's approach to decoupled content management is elegantly thought out.

Speaking with the Sanity creators at the JAMstack conference in New York this year, we confirmed two things we've always known about them: First, they're were super friendly and ridiculously smart; and second, Sanity just keeps getting sleeker and more intuitive despite being less than 2 years old.

Seriously, Sanity is crushin' it right now.

BTW, huge H/T to Chris Earls for sharing Sanity in a Slack room where some Snipcart team members lurk around.

But enough with the small talk. Let's get to buildin'.

Angular e-commerce with Sanity, Netlify, and Snipcart

angular-sanity-updated

First of all, we'll use the amazing Angular CLI to create a new Angular project.

ng new snipcart-angular-sanity

You'll notice that it will create you a bunch of folders and files, but this is the way to go when starting a new Angular project. The CLI really makes it simpler and works very well.

Before diving into Angular itself, we'll go through the setup steps:

  • Setting up Sanity
  • Setting up Netlify
  • Create a new Netlify function to fetch products from Sanity

Setting up Sanity

Then, we'll install Sanity CLI. This will be helpful to generate our new Sanity CMS instance.

npm install -g @sanity/cli

Then, create a new Sanity project:

sanity init

We'll use the e-commerce project template. This will generate some models to start with with some dummy data (hence the name of our online store will be "Keyway"). This can be useful to make development faster!

Sanity's CLI output should look like this:

Sanity-cli-updated

Now, start your Sanity studio:

cd keyway
sanity start

You should be able to open the studio in your browser and add some data.

For this tutorial, I deployed the studio on Sanity's hosted service. This will be useful to manage your content once the site is deployed. You can read more about how to deploy Sanity studio in their documentation

Now, open Sanity's dashboard and navigate to your project. Under the settings tab, click on API, then generate a new token:

sanity-add-new-token-updated

Name this token "Functions" and hit "Add New Token":

sanity-new-token-updated

Setting up Netlify

We'll use Netlify to host our site, but most importantly, we'll use it as a proxy between Sanity and our application. We don't want to expose all of our data to the wild, so we're going to make everything private on Sanity's end, and expose exactly what we want through Netlify functions. We'll call these functions from the frontend later on.

Start by creating a file named netlify.toml at the root of your project. This is where we'll configure some settings for Netlify.

[build]
  command = "npm run build"
  publish = "dist/snipcart-angular-sanity"
  functions = "src/functions"

Basically, we're setting up the build command, the directory to publish, and the folder that will contain the functions we'll create.

netlify init

Follow the steps to create your Netlify project. Once it's done, open your site, go to Environment variables and add these ones:

  • SANITY_TOKEN: The token you generated in the first step of this post.
  • SANITY_DATASET: The name of the dataset you created, if you named it like I did, it should be production.
  • SANITY_PROJECT_ID: The ID of your Sanity project that can be found when you open the project in Sanity's dashboard.

To locate your project ID, open your Sanity project and look for this:

sanity-project-id-updated

For this project, we'll also use Netlify Dev. It's a new tool from Netlify that allows you to run their whole infrastructure locally on your computer. This makes writing and testing functions very easy. It's the first time I've used it and, I have to say, it's awesome.

Open your netlify.toml file and add this config:

[dev]
  command = "npm start"
  port = 4200
  publish = "src"
  functions = "src/functions"

Then, you can run netlify dev. It's going to start your Netlify dev server, and your Angular app as well.

Create a function to fetch products

In the src folder, create a new folder named functions. This is where we'll create our function, as specified in the netlify.toml file.

Create a new file named getProducts.js.

We'll start our function like this:

exports.handler = (event, context, callback) => {
  callback(null, {
    statusCode: 200,
    body: 'Hello world'
  });
}

Now, if you have your Netlify dev server running, you should be able to open your function in your browser at http://localhost:<netlify_port>/.netlify/functions/getProducts.

You should see an awesome "Hello world" message.

Now, let's get products from Sanity. In this function we'll need 3 packages created by the folks at Sanity:

  • @sanity/client: The client to interact with their API, this is the package that will allow us to fetch products.
  • @sanity/image-url: This package is helpful to generate URLs for your images stored in Sanity.
  • @sanity/block-content-to-html: This package translates a Sanity Block Content to HTML.
npm install @sanity/client --save
npm install @sanity/image-url --save
npm install @sanity/block-content-to-html --save

Now, go back to getProducts.js and instantiate your Sanity client:

const sanity = sanityClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET,
  token: process.env.SANITY_TOKEN,
  useCdn: true
});

Since you're using Netlify Dev and have defined environment variables into your site, these variables will automatically be available locally. So you don't have to worry about environment files whatsoever.

The final code of your function should look like this:

const sanityClient = require('@sanity/client');
const imageUrlBuilder = require('@sanity/image-url');
const blocksToHtml = require('@sanity/block-content-to-html');

const sanity = sanityClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET,
  token: process.env.SANITY_TOKEN,
  useCdn: true
});

exports.handler = (event, context, callback) => {
  const query = '*[_type=="product"] | order(title asc)';
  sanity.fetch(query).then(results => {
    const products = results.map(x => {
      const output = {
        id: x.slug.current,
        name: x.title,
        url: `${process.env.URL}/.netlify/functions/getProducts`,
        price: x.defaultProductVariant.price,
        description: x.blurb.en,
        body: blocksToHtml({blocks: x.body.en}),
      }

      const image = x.defaultProductVariant.images && x.defaultProductVariant.images.length > 0
        ? x.defaultProductVariant.images[0].asset._ref
        : null;

      if (image) {
        output.image = imageUrlBuilder(sanity).image(image).size(300, 300).fit('fillmax').url();
      }

      return output;
    });

    callback(null, {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(products),
    });
  });
}

As you can see, we're using the fetch method available on the SanityClient instance, and we're passing this query as a parameter:

*[_type=="product"] | order(title asc)

This query language has been introduced by the team at Sanity, and it makes it very simple to query your data because they have a bunch of helpers, transformers and other useful things to help with that. You can read more about it right here.

Then, we map the results to an interface that we'll use in our Angular application:

const products = results.map(x => {
  const output = {
    id: x.slug.current,
    name: x.title,
    url: `${process.env.URL}/.netlify/functions/getProducts`,
    price: x.defaultProductVariant.price,
    description: x.blurb.en,
    body: blocksToHtml({blocks: x.body.en}),
  }
  return output;
});

We use the blocksToHtml method available through @sanity/block-content-to-html library here to convert Sanity's content to HTML.

Then, we'll use the @sanity/image-url to generate the image URL we'll use in our application.

const image = x.defaultProductVariant.images && x.defaultProductVariant.images.length > 0
  ? x.defaultProductVariant.images[0].asset._ref
  : null;
if (image) {
  output.image = imageUrlBuilder(sanity).image(image).size(300, 300).fit('fillmax').url();
}

Lastly, we return the payload:

callback(null, {
  statusCode: 200,
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(products),
});

We have now assembled all the "backend" pieces required to build our application. So, let's dive into Angular itself.

You can test it by calling the function with a tool like Postman or Insomnia:

function-output-updated

One cool thing about this is that we can use our function URL as data-item-url value since the format we followed will work with the JSON crawler.

Building the Angular app

For this project, we used a neat little template for the store. If you clone our repository, you'll get all the necessary assets.

Update the index.html file so it looks like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="x-ua-compatible" content="ie=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
    <title>Keyway</title>
    <base href="/" />
    <meta name="description" content=""/>
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <meta name="fragment" content="!"/>
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <meta name="theme-color" content="#408c71">
    <style>body { background-color: #f2f2f2 }</style>

    <link rel="stylesheet" href="https://cdn.snipcart.com/themes/v3.0.0-beta.1/default/snipcart.css" />
  </head>
  <body>
    <app-root></app-root>

    <div hidden id="snipcart" data-api-key="MzMxN2Y0ODMtOWNhMy00YzUzLWFiNTYtZjMwZTRkZDcxYzM4"></div>
    <script src="https://cdn.snipcart.com/themes/v3.0.0-beta.1/default/snipcart.js"></script>
  </body>
</html>

As you can see, I included the version 3 of Snipcart. We just released the beta and I thought it'd be nice to use it in a demo like this one 😀

Create nav bar

We'll now add a navigation bar into our application, open app.component.html and copy this code:

<div>
  <nav class="nav">
    <a href="/"><img class="nav__logo" src="assets/png/logo.png" alt="Keyway" width="190px"/></a>
    <button class="snipcart-checkout">
      View cart
    </button>
  </nav>
</div>

Then, we'll insert the router-outlet component:

<div>
  <nav class="nav">
    <a href="/"><img class="nav__logo" src="assets/png/logo.png" alt="Keyway" width="190px"/></a>
    <button class="snipcart-checkout">
      View cart
    </button>
  </nav>

  <router-outlet></router-outlet>
</div>

Fetching products

We'll now create a service named Products, this service will be responsible for fetching product information from the backend.

First of all, we'll need to import the HttpClientModule. This module will make it possible to import a HttpClient similar to Axios into any of our services or components.

Open app.module.ts and we'll import the module itself.

import { HttpClientModule } from  '@angular/common/http';

Then, add it to the imports array:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

We're now ready to create the service. In your terminal, use Angular CLI:

ng generate service services/Products

Then, open products.service.ts and paste this:

import { Injectable } from '@angular/core';
import { Product } from '../models/Product';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class ProductsService {
  constructor(private http: HttpClient) { }

  getAllProducts(): Observable<Product[]> {
    return this.http.get<Product[]>('/.netlify/functions/getProducts', {
      headers: {
        'Content-Type': 'application/json'
      },
    });
  }
}

As you can see, we inject the HttpClient into the service constructor. The client methods like get or post always return an observable. RxJs is a core element of Angular.

So you need to create a new method called getAllProducts that will return an Observable<Product[]>. Don't forget to pass the Content-Type header.

The URL will be the URL to the Netlify function that we created earlier.

Create ProductsList component

Now it's time to create our first component. This component will render a products list.

In your terminal, type:

ng generate component components/ProductsList

Then, open products-list.component.ts and inject ProductsService in the component's constructor.

import { Component, OnInit } from '@angular/core';
import { ProductsService } from 'src/app/services/products.service';

@Component({
  selector: 'app-products-list',
  templateUrl: './products-list.component.html',
  styleUrls: ['./products-list.component.scss']
})
export class ProductsListComponent implements OnInit {
 
  constructor(private productsService: ProductsService) { }

  ngOnInit() {
  }
}

Then, we'll add two properties to the component: products which will be the array containing the products to display, and loading that will show a loading message while products are being retrieved from the server.

import { Component, OnInit } from '@angular/core';
import { ProductsService } from 'src/app/services/products.service';
import { Product } from 'src/app/models/Product';

@Component({
  selector: 'app-products-list',
  templateUrl: './products-list.component.html',
  styleUrls: ['./products-list.component.scss']
})
export class ProductsListComponent implements OnInit {
  products: Product[] = [];
  loading = false;

  constructor(private productsService: ProductsService) { }

  ngOnInit() {
  }
}

Then, we'll fetch products and handle the loading state in the ngOnInit lifecycle hook.

import { Component, OnInit } from '@angular/core';
import { ProductsService } from 'src/app/services/products.service';
import { Product } from 'src/app/models/Product';

@Component({
  selector: 'app-products-list',
  templateUrl: './products-list.component.html',
  styleUrls: ['./products-list.component.scss']
})
export class ProductsListComponent implements OnInit {
  products: Product[] = [];
  loading = false;

  constructor(private productsService: ProductsService) { }

  ngOnInit() {
    this.loading = true;

    this.productsService.getAllProducts()
      .subscribe(products => {
        this.loading = false;
        this.products = products;
      });
  }
}

Finally, we'll write the template necessary to display the products. Open products-list.component.html and paste this:

<main class="catalog" *ngIf="!loading; else loadingBlock">
    <app-products-list-item class="catalog__product"
      *ngFor="let product of products"
      [product]="product">
        {{ product.name }}
    </app-products-list-item>
</main>

<ng-template #loadingBlock>
  <main class="catalog catalog--loading">
    <span class="catalog__loading">
      Loading products...
    </span>
  </main>
</ng-template>

Notice the app-products-list-item component usage? We'll create this one right away.

Create ProductsListItem component

In your terminal, type:

ng generate component components/ProductsListItem

Now open products-list-item.component.ts; the ProductsListItem component needs to have one input property: product. Let's add this to our component TypeScript file.

import { Component, OnInit, Input } from '@angular/core';
import { Product } from 'src/app/models/Product';

@Component({
  selector: 'app-products-list-item',
  templateUrl: './products-list-item.component.html',
  styleUrls: ['./products-list-item.component.scss']
})
export class ProductsListItemComponent implements OnInit {

  @Input() product: Product;

  constructor() { }

  ngOnInit() {
  }
}

Then, we'll write the template for this component. In the template, we'll create a Snipcart Buy Button. So we'll need to specify some data properties:

<figure class="product">
  <img [src]="product.image"/>

  <figcaption>
    <h3 class="product__name">{{ product.name }} <span class="product__price">{{ product.price }}$</span></h3>
    <button class="button button--buy snipcart-add-item"
      [attr.data-item-url]="product.url"
      [attr.data-item-id]="product.id"
      [attr.data-item-price]="product.price"
      [attr.data-item-description]="product.description"
      [attr.data-item-image]="product.image"
      [attr.data-item-name]="product.name">
      Add to cart
    </button>
  </figcaption>
</figure>

We now have all our necessary components. Let's wire it all together.

Configure our default route.

The default route will render the ProductsList component. Open app-routing.module.ts and add this route to the routes array:

import { ProductsListComponent } from  './components/products-list/products-list.component';

const routes: Routes = [{
  path: '',
  component: ProductsListComponent,
}];

Start your dev server

Open a terminal, and type: netlify dev. This will start your Netlify server along with the Angular application. You should then be able to access your app in your browser.

This should look like this:

shop-screenshot-updated

See the live demo

See the Github repo

Conclusion

I had a lot of fun writing this demo. Overall, it took me about 2 days to put everything together. But, I had never worked with Sanity, and never really used Angular, except back in the days with their first version.

At the beginning, I wasn't sure about Angular. I thought that they had too much boilerplate code and it was almost scary just looking at an Angular e-commerce project because of all the files they usually contain ;) However, the Angular CLI is really neat and it makes development so much easier.

I recommend this video on Youtube if you want to get a crash course on how it works.

Netlify and Sanity have also been very straightforward. The more I learn about Sanity, the more I like it.

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

Suggested posts: