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

In a rush? Skip to tutorial steps or live demo.

Do you eat cereals?

I bet you do.

Ever literally froze in place in front of the store's cereals section? I have.

I mean, there are TONS of boxes. Always one or two new kinds. They all look good. And you look just... ridiculous, trapped in tragic decision paralysis.

JS frameworks and headless CMS are a bit like cereal boxes. Eventually, you just got to pick one—the right ones, if possible.

choosing-js-frameworks-headless-cms

For Pete's sake, just pick one already!

I spent way too much time looking at different tools for this headless e-commerce tutorial. I eventually picked some brand spanking new tooling: Angular 5.0 & Sanity CMS.

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

This post will cover how to:

  1. Set up Sanity with a products schema (data modeling)
  2. Input real products data in the headless CMS
  3. Build an Angular app with appropriate components to display storefront

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

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

So what's up with Angular?

angular-overview

Angular is an open source JS framework developed by Google engineers in 2010. While its lexical proximity to AngularJS—its official, now incompatible v1.0—might confuse some, it is a whole different framework.

Angular's full, bumpy development roadmap can be found on Wikipedia.

Its focus on data binding & dependency injection makes it a solid tool for single page applications (SPA). Angular is also becoming a go-to for multi-platform deployment, be it web, mobile, or desktop. In that sense, it's a perfect fit for our current headless e-commerce use case. As a matter of fact, a team of talented French devs has recently shipped a Snipcart/Angular e-commerce set up on top of a headless Drupal backend. (Case study in the works!)

Angular's latest 5.0 release focuses on making it smaller, faster & easier to use.

The framework doesn't appear to be going away anytime soon either:

angular-overall-popularity

Google Trends report

angular-usage

BuiltWith usage trend for Angular 2.0^

I had never used Angular before crafting this tuts and was intrigued by its latest release. Hence why I picked this cereal box over another. I've now got heaps to say about Angular, but I don't want to influence your opinion just yet. So I'll keep my editorial comments for later. :P

What do you mean by "headless e-commerce," huh?

headless-ecommerce-architecture

Headless e-commerce architecture

Bear with me, we're exiting the "coupled" Magento/Shopify/WooCommerce land here.

If you follow our blog a little, you might have caught our takes & tuts on headless CMS. If not, no biggie, you can start with this primer on API-first, headless CMS.

Simply put, going "headless" means decoupling the backend from the frontend. Why would one do that, you ask? My teammate Franck has already written a solid answer to this on Quora, so allow me to quote him:

  • Unlike a traditional CMS, an API-first CMS exposes its content data via a consumable API. So you can use raw content and display it however/wherever you want (mobile, static site, web app, desktop app, etc.). [...]
  • You avoid known (and multiple) security exploits that pile up on top of popular CMS like WordPress.
  • You can easily do cross-platform content management and minimize costs for doing so.
  • If you're using a static site for rendering your content, a headless CMS lets non-techie users edit, create, manage content in an intuitive UI instead of struggling in .md files and Git repos.
  • If you're a developer, you can actually have fun building a site tailored to your or your clients' needs (ditching CMS templates, for instance).
  • You avoid costly site migrations by decoupling your content from your frontend and storing it in a dedicated API.

Still not convinced about adopting a decoupled architecture? Give this JAMstack talk a listen. ;)

Quick word on Sanity's (awesome) headless CMS

headless-cms-sanity

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 on Gitter a few weeks ago, we learned that A) they were super friendly and B) Sanity was literally just a few weeks old! Doesn't get much fresher than that, huh?

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

Tutorial: Angular e-commerce with headless Sanity & Snipcart

angular-sanity-snipcart-headless-ecommerce

Prerequisites

1. Scaffolding the headless e-commerce app

Let's install both Angular and Sanity cli to run everything from the terminal:

npm install -g @angular/cli
npm install -g @sanity/cli

Now run sanity init where you want your project to be located.

Here's how your terminal should look:

sanity-ini-terminal

In the same dev folder, we'll generate a new Angular project with ng new {project_name}. For now, we won't touch this one and focus on Sanity.

Boot up your favorite editor in Sanity's folder.

2. Creating the products schema

Sanity uses a schema to define its collections format.

We'll need to add a new one to model our shop's products. Open up the schema folder, and nest a new product folder inside. In the latter, create a new index.js file and export a single object:

export default {
    title: 'Product',
    name: 'product',
    type: 'document',
    fields: [
      {
        title: 'Name',
        name: 'name',
        type: 'string'
      },
      {
        title: 'Description',
        name: 'description',
        type: 'string'
      },
      {
        title: 'Price',
        name: 'price',
        type: 'number'
      },
      {
        title: 'Image',
        name: 'image',
        type: 'image'
      }
    ]
}

The above schema represents Snipcart's products attributes. This is only mere product data that will be parsed into active buy buttons later on, through our Angular app.

It is quite simple, but rest assured, Sanity allows quite complex data modeling too.

Now hop in the schema.js file and refactor it:

const createSchema = require('part:@sanity/base/schema-creator')
const schemaTypes = require('all:part:@sanity/base/schema-type')
import product from './product'

module.exports = createSchema({
  name: 'default',
  types: schemaTypes.concat([product])
})

Here, we only introduced the product import and added it to the types array.

Back in your terminal, in your Sanity project folder, input: sanity start.

This will run the Content Studio locally on your machine. Were you more than one developer on the project, you could also deploy it. The whole studio is open source, making it easy to extend. It's also designed so you can work collaboratively on any entity in the studio—pretty neat.

3. Adding the products in Sanity

By default, the studio should be running on http://localhost:3333.

You should now see a product tab in the content section. Click on it; a product creation form based on your schema structure should appear:

create-products-sanity-studio

For the demo, we created three Christmas decoration products. You can create whatever you want!

After defining products, we only need to fetch and display them with our Angular app.

It might not be clear at first, but once you save anything in the studio, it will automatically push changes to Sanity's server.

You should be all set with Sanity now; let's jump into the Angular e-commerce app.

4. Building the Angular app & components

Fire up your editor again, but this time in the Angular project.

We won't dive deeply into Angular concepts here, but if you're lost at some point, refer to their bulletproof docs.

We'll only need two components for this demo: products listing and product details.

Insert a components folder in /src/app. In it, create a products and a product folder.

In the first one, add a products.component.html file and a products.component.ts one. In the second one, add a product.component.html file and a product.component.ts one.

Let's start by opening products.component.ts.

Copy the following content inside it:

import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Query } from '@angular/core/src/metadata/di';

@Component({
  templateUrl: './products.component.html',
  styleUrls: ['./../../app.component.css'],
})
export class ProductsComponent implements OnInit {
  
  constructor(private http: HttpClient) {}
  
  products: object[];

  ngOnInit(): void {
    var query = "*[_type == 'product']{ name, _id, description, price, 'imageUrl': image.asset->url }";

    this.http
      .get(`https://7abtqvex.apicdn.sanity.io/v1/data/query/products?query=${query}`)
      .subscribe(data => {
        this.products = data["result"];
      });
  }
}

If you don't have a lot of Angular experience, this might feel heavy at first. I think that's because Angular is quite opinionated and heavily object-oriented. This approach can be practical for full-scale projects, but it's a little hefty for our use case, I'll admit.

Still, it's always good to play around with different tech stacks to make your own opinion! :)

On the plus side, Angular is quite verbose, so even if you don't have experience, you can infer what's happening easily. In our case, we're simply using the OnInit component method to fetch all products and assign it to the products variable. If the query feels alien to you, check out Sanity's docs on GROQ queries.

Let's jump in the products.component.html file now, which is simply the template our products component will use to render itself:

<div class="products">
    <div *ngFor="let product of products" class="product">
        <a class="product" routerLink="product/{{ product._id }}">
            <img src="{{ product.imageUrl }}" height="200"/>
            <p class="product-name">{{ product.name }}</p>
        </a>

        <button class="snipcart-add-item"
            attr.data-item-name="{{ product.name }}"
            attr.data-item-id="{{ product._id }}"
            attr.data-item-image="{{ product.imageUrl }}"
            attr.data-item-price="{{ product.price }}"
            attr.data-item-description="{{ product.description }}"
            data-item-url='https://wt-4b2a879e3bb5c630d65bc12c99321764-0.run.webtask.io/sanity-parser'>
            Buy it for {{ product.price }}$
        </button>
        
    </div>
</div>

Below you'll find, in order, our product.component.ts file and our product.component.html file (product details).

They're quite similar to the products listing one, so I won't explain them.

import { Component } from '@angular/core';
import { OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './product.component.html',
  styleUrls: ['./../../app.component.css'],
})
export class ProductComponent implements OnInit {
  
  constructor(
      private http: HttpClient,
      private route: ActivatedRoute) {}
  
  product: object;

  setProduct(id): void {
    var query = `*[_id == '${id}']{ name, _id, description, price, 'imageUrl': image.asset->url }`;

    this.http
        .get(`https://7abtqvex.apicdn.sanity.io/v1/data/query/products?query=${query}`)
        .subscribe(data => {
            this.product = data["result"][0];
        });
  }

  ngOnInit(): void {
    this.route.url
        .subscribe(curr => {
            this.setProduct(curr[1].path);
        })
  }
}
<div *ngIf="product" class="product">
    <img src="{{ product.imageUrl }}" height="400"/>
    <p>{{ product.name }}</p>
    
    <button class="snipcart-add-item"
    attr.data-item-name="{{ product.name }}"
    attr.data-item-id="{{ product._id }}"
    attr.data-item-image="{{ product.imageUrl }}"
    attr.data-item-price="{{ product.price }}"
    attr.data-item-description="{{ product.description }}"
    data-item-url='https://wt-4b2a879e3bb5c630d65bc12c99321764-0.run.webtask.io/sanity-parser'>
    Buy it for {{ product.price }}$
</button>    
</div>

5. Bundling this whole JAMstack e-commerce

So, components: check.

Let's define the proper flow to render them.

We'll do this in the already defined app.module.ts file.

We'll use the Angular router to make things easy. Import it as follows:

import { RouterModule, Routes } from '@angular/router';

We'll also need to include our components with:

import { ProductComponent } from './components/product/product.component';
import { ProductsComponent } from './components/products/products.component';

For our demo, we are also using the HttpClientModule to help us with HTTP calls. Import it with:

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

Then, define your routes:

const appRoutes: Routes = [
  { path: 'product/:id', component: ProductComponent },
  { path: '', component: ProductsComponent }
]

This states that the index page should render the ProductsComponent and that pages with the format /product/:id should use the ProductComponent.

You'll need to include those parts in the properties of the @NgModule. To make things clearer, I'll put the entire file declaration here:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent } from './app.component';
import { ProductComponent } from './components/product/product.component';
import { ProductsComponent } from './components/products/products.component';

const appRoutes: Routes = [
  { path: 'product/:id', component: ProductComponent },
  { path: '', component: ProductsComponent }
]

@NgModule({
  declarations: [
    AppComponent,
    ProductComponent,
    ProductsComponent
  ],
  imports: [
    RouterModule.forRoot(appRoutes),
    BrowserModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [ AppComponent ]
})

export class AppModule { }

We did specify components to be rendered for routes, but we haven't yet declared where the app should render them. Open the app.component.html file and overwrite the content as such:

<div style="text-align:center">
  <a href="/">
    <h1>
      Snipcart & Sanity powered demo!
    </h1>
  </a>
  <router-outlet></router-outlet>
</div>

The router-outlet tag will serve as a placeholder telling Angular where the route component should be rendered.

And we're done!

Use the ng serve method to start a server locally and see the final product. Remember that for Sanity to serve the data to any website, you will have to add your website's host in their allowed CORS Origin. You can do so in your Sanity account, in the settings section of your project.

One last thing: we didn't include any Snipcart-specific scripts yet. Let's to do it so our buy buttons can actually work. Hop in the index.html file and add the following lines at the end of the body:

<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="https://cdn.snipcart.com/scripts/2.0/snipcart.js" data-api-key="YjdiNWIyOTUtZTIyMy00MWMwLTkwNDUtMzI1M2M2NTgxYjE0" id="snipcart"></script>
<link href="https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css" rel="stylesheet" type="text/css" />

GitHub repo & live demo

headless-angular-ecommerce-demo

And here's our final result!

See the open source repo on GitHub

See live demo deployed on Netlify

Closing thoughts

That was quite the setup, wasn't it? Took me about 2-3 hours (that's a lie: including all the reading I had to do, it took me at least a full day).

I really enjoyed Sanity: it's neat, and I like the Content Studio approach.

Note: in the demo, we did the queries by hand to keep things as simple as possible for a small project. However, Sanity has developed a useful JS client to help you interact with their API. If you were to build a real project, I would highly recommend using the library instead. You can read more about this here.

Angular was a bit more disappointing, at least for me. I wasn't too fond of the heavy object-oriented approach: it forces a steep first step to play with the framework and a lot of documentation reading. Although I know this can be quite useful for large team/scale projects.

I felt like there was an underlying contradiction between JavaScript's non-paradigm-specific approach and Angular's super opinionated implementation. But again, I can see how helpful that can be to maintain code uniformity across big teams. I'm not sure I'd use it for small-scale projects though!

But if you dig Angular, I'm convinced you can come up with solid e-commerce systems. If you do, send them our way, we'd love to have a look! :)


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

Suggested posts: