GSD

Headless Commerce: Building a Shopify Store With Angular

Posted by Orly Knop on September 27, 2023

pexels-edgars-kisuro-1488467.jpg

What is Headless Commerce?

Headless Commerce is the concept of separating your store’s front-end from the e-commerce platform in an effort to better customize the user experience for your customers. By separating the two, you can greatly enhance the performance and user experience for your customers while still utilizing the core features of Shopify that help you sell your products. By going this route, you can integrate a headless CMS with your store to build dynamic, content-rich landing pages and promotional pages that better complement your products.

In this tutorial, we will create a headless Shopify store using The Shopify Storefront API and ButterCMS. Shopify’s Storefront API allows you to add Shopify buying experiences to any website. By integrating ButterCMS with your site, you’ll be able to create unique marketing content such as promotional pages, SEO landing pages, and much more to solidify your brand and improve your business’s performance.  We'll show you how to build a simple promotional page:

undefined

This tutorial will cover how to use the Shopify Storefront API to create a headless Shopify store integrated with ButterCMS to create promotional pages. We will show you how to build your headless commerce site in-depth, covering the following:

  • Create a private Shopify application
  • Display store collection products in an Angular application
  • Synchronize a collection from ButterCMS, a headless CMS for Shopify, with your Shopify products using Node.js and Express
  • Integrate ButterCMS into the storefront by creating and using ButterCMS page types

The full code is available in the GitHub repository https://github.com/ButterCMS/headless-shopify, and a demo is available at https://cranky-galileo-d83d7f.netlify.app/.

blog-cta-angular-redborder_800x100.png

Create a private Shopify app

You will need to create a private Shopify app in order to build a headless commerce site with Shopify.  This private Shopify app is created by and for your single Shopify store and it cannot be sold in the Shopify marketplace. 

To create a private Shopify app, we need to have a store. If you don’t have one yet, you may create a development store using your Shopify partner account.

undefined

Once the store is created, go to your store’s Apps page and click “Manage private apps”.

undefined

Click the “Create new private app” button, and fill the “Private app name” and “Emergency developer email” fields. These are required. In the “Storefront API” section, check the box that says “Allow this app to access your storefront data using the Storefront API”. We will need permissions specified in the first three checkboxes and also “Read and modify checkouts”. Check the checkboxes as shown in the screenshot, click “Save”, and then click “Create App”.

undefined

When the app is successfully created, you will see a list of values that might be needed during app development. To use the Shopify Storefront API, we will need a Storefront access token. As noted on our app page, this value is not secret and can be used for our Angular application.

Display store collection products in an Angular application

It’s a good practice to store the Storefront access token and the store URL as environment variables and create a service called, for example, EnvironmentVariablesService, which would return these values.

The Storefront API is available in GraphQL. To make requests to the API from the Angular application, we will use Apollo Angular. Run this command to add Apollo to your app:

ng add apollo-angular

This command will make several changes to the project, including a generation of a module graphql.module.ts. Since we need to add the X-Shopify-Storefront-Access-Token header to requests, and the link for the requests depends on the Shopify store URL, this file should be changed. We will need apollo-link-context, so run this command:

npm install apollo-link-context 

Add EnvironmentVariablesService as a dependency to the APOLLO_OPTIONS provider:

@NgModule({
  exports: [ApolloModule, HttpLinkModule], 
providers: [

    {
      provide: APOLLO_OPTIONS,
      useFactory: createApollo,
      deps: [HttpLink, EnvironmentVariablesService],
    },
  ],
})
export class GraphQLModule {}


Then, update the createApollo function:

export function createApollo(
  httpLink: HttpLink,
  environmentVariablesService: EnvironmentVariablesService
): {
  link: ApolloLink;
  cache: InMemoryCache;
} {
  const uri = `${environmentVariablesService.storeUrl}/api/graphql.json`;
  const headers = setContext((operation, context) => ({
    headers: {
      'X-Shopify-Storefront-Access-Token':
        environmentVariablesService.storeFrontToken,
    },
  }));

  const link = ApolloLink.from([headers, httpLink.create({ uri })]);
  const cache = new InMemoryCache();

  return {
    link,
    cache,
  };
}

By making this change, we added an X-Shopify-Storefront-Access-Token header to requests and set the request URL as `${environmentVariablesService.storeUrl}/api/graphql.json`

Create ShopifyService with Apollo as a dependency, and add a new method getProducts:

// shopify.service.ts


getProducts(
  collectionHandle: string
): Observable<ApolloQueryResult<CollectionProducts>> {
  return this.apollo.query<CollectionProducts>({
    query: gql`
      {
        collectionByHandle(handle: "${collectionHandle}") {
          products(first: 10) {
            edges {
              node {
                id
                title
                description
                handle
              }
            }
          }
        }
      }
    `,
  });
}

In this method, we only retrieve the first 10 collection products. If you want to get all products and don’t know how to do that, read this Shopify Community question. The API provides more product properties such as images, prices, etc.

Create a component to display the products:

@Component({
  selector: 'product-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.scss'],
})
export class ProductsComponent implements OnInit {
  products: Observable<ApolloQueryResult<Products>>;

  constructor(private shopifyService: ShopifyService) {}

  ngOnInit(): void {
    this.products = this.shopifyService.getAllProducts();
  }
}

In the component template, products can be displayed by iterating through the (products | async)?.data.products.edges value.

blog-cta-angular-redborder_800x100.png

Headless Shopify: Synchronizing Shopify products with a ButterCMS collection

A great looking promotional page will entice customers to view products from your store, therefore linking to specific products tied to the theme of your promotional page is a basic requirement.  Shopify has webhooks that are sent every time a product is created or updated. Since we want to store products’ images in a ButterCMS collection, we should use a webhook for product update events. Webhooks for product creation events might not contain the image because it takes longer for the image data to process than it does for the webhook to fire (more info here).

We should decide which product data to store in our collection. The full list of data provided by the webhook is available here. When our route for the webhook is requested, we should verify that it was sent by our Shopify store. This can be done by using shopify-hmac-validation. Verification requires a raw body value, so we should take this into account when creating the route.

If the request is from the store, we should create a new item in the ButterCMS collection by making a POST request to https://api.buttercms.com/v2/content/. We need a write-enabled token to make this request. This token should be kept secret, so store it in the .env file.

Create a collection for store products in ButterCMS

Go to https://buttercms.com/content/collections/create and create a collection with item elements, as shown in the screenshot.

undefined

Create a webhook for the headless Shopify store

Go to your Shopify store settings and click on “Notifications”. 

undefined

In the webhooks section, click “Create webhook” and in the form, select the event “Product update.” Type in the route for the webhook you want to use and click “Save webhook”. For development, use ngrok to create a tunnel for localhost. When you create your first webhook, you’ll see a note saying “All your webhooks will be signed with XXXXXXX so you can verify their integrity.” Save this value in the .env file as WEBHOOK_SECRET.

undefined

Create a route for the created webhook

We will create a server using Node.js and Express. Add the name of the collection you created for the products list to the .env file as the variable 

PRODUCTS_COLLECTION_NAME.

We want our product collection to contain the product price. Since products may have variants with different prices, we could make the price be a certain number or have a range, like “from 10 USD”, depending on the variants’ prices. We can calculate the minimum price by going through variants’ prices or by querying the Shopify Storefront API. In this tutorial, we will query the API.

Install packages for the server by running this:

npm install express body-parser shopify-hmac-validation request-promise graphql-request

When this article was written, the latest version of graphql-request caused an error when a request was made. So I used graphql-request@^1.8.2  instead.

Create a file named server.js and copy and paste the following code:

// server.js

const express = require("express");
const bodyParser = require("body-parser");
const hmacValidity = require("shopify-hmac-validation");

const { ButterCMSService } = require("./butter-cms-service");
const { ShopifyService } = require("./shopify-service");

const app = express();
const apiRoute = "";
const butterCMSService = new ButterCMSService();
const shopifyService = new ShopifyService();

function rawBodySaver(req, res, buf, encoding) {
  if (buf && buf.length) {
    req.rawBody = buf.toString(encoding || "utf8");
  }
}

function verifyWebhookRequest(req, res, next) {
  try {
    if (
      !hmacValidity.checkWebhookHmacValidity(
        process.env.WEBHOOK_SECRET,
        req.rawBody,
        req.headers["x-shopify-hmac-sha256"]
      )
    ) {
      throw Error("Unauthorized request");
    }
    return next();
  } catch (e) {
    console.log(e);
    return res.status(401).json({ message: "Unauthorized request" });
  }
}

app.use(
  `${apiRoute}/webhooks/product-update`,
  bodyParser.json({ verify: rawBodySaver })
);

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

const router = express.Router();

router.post(
  "/webhooks/product-update",
  verifyWebhookRequest,
  async (req, res) => {
    try {
      const priceRange = await shopifyService.getProductPriceRange(
        req.body.handle
      );
      const price = `${
        priceRange.minVariantPrice.amount != priceRange.maxVariantPrice.amount
          ? "from "
          : ""
      } ${priceRange.minVariantPrice.amount} ${
        priceRange.minVariantPrice.currencyCode
      }`;
      await butterCMSService.addItemToCollection(
        process.env.PRODUCTS_COLLECTION_NAME,
        {
          title: req.body.title,
          image: req.body.image ? req.body.image.src : "",
          description: req.body.body_html,
          image_alt: req.body.title,
          price,
        }
      );
      return res
        .status(200)
        .json({ message: "Product has been added to collection" });
    } catch (error) {
      console.log(error);
      return res.status(400).json({ message: "Server error" });
    }
  }
);

app.use(apiRoute, router);

const port = process.PORT || 3000;
app.listen(port, () => console.log(`Local app listening on port ${port}`));

Note that the addItemToCollection method requires an object with properties that match the collection item structure.

Add your ButterCMS write-enabled token to the .env file as the variable BUTTER_CMS_TOKEN, and create butter-cms-service.js in the same directory as server.js with the following content:

// butter-cms-service.js

const requestPromise = require("request-promise");

class ButterCMSService {
  async addItemToCollection(collectionName, item) {
    const writeToken = process.env.BUTTER_CMS_TOKEN;
    if (!writeToken) {
      return Promise.reject(Error("No write-enabled token configured"));
    }
    const body = {
      key: collectionName,
      status: "published",
      fields: [
        {
          en: item,
        },
      ],
    };
    const options = {
      method: "POST",
      uri: "https://api.buttercms.com/v2/content/",
      body,
      json: true,
      headers: {
        authorization: `Token ${writeToken}`,
      },
    };
    return requestPromise(options);
  }
}

module.exports = { ButterCMSService };


Add an environment variable named
SHOP_URL with the store’s URL and APP_TOKEN with the value of the Storefront access token. Create shopify-service.js in the same directory as server.js with this code:

// shopify-service.js

const { GraphQLClient } = require("graphql-request");

class ShopifyService {
  constructor() {
    const endpoint = `${process.env.SHOP_URL}/api/graphql.json`;

    this.graphQLClient = new GraphQLClient(endpoint, {
      headers: {
        "X-Shopify-Storefront-Access-Token": process.env.APP_TOKEN,
      },
    });
  }

  async getProductPriceRange(productHandle) {
    const query = `{
      productByHandle(handle: "${productHandle}") {
        priceRange {
          minVariantPrice {
            amount
            currencyCode
          }
          maxVariantPrice {
            amount
            currencyCode
          }
        }
      }
    }`;
    const response = await this.graphQLClient.request(query);
    if (response.errors) {
      throw response.error;
    }
    return response.productByHandle.priceRange;
  }
}

module.exports = { ShopifyService };


To verify that the created webhook works, either click “Send test notification” on the page where we created the webhook in the store, or update any of your products. After several seconds, a new item containing updated product information should be added to your collection.

Integrate ButterCMS pages into your headless Shopify store

We want to easily create promotional pages for our custom storefront: when a page is published in ButterCMS, this page should be available from our store. To do this we need to create in the custom Shopify store a route pages/{slug}, where {slug} is a slug of the ButterCMS page. When a user goes to this route, we need to fetch the page content from ButterCMS and display it. The page content is defined by the promotional page type structure.

Create a ButterCMS page type

We want the page type to have a list of products that are stored in the created products collection. To create a new Page Type, we should go to https://buttercms.com/pages/, click the “New page” button, and then click “Create new page”. You will be redirected to a new page where you can build a page.

Click “Component” and a new block should appear. Click “Add from Library” and select “SEO” in the list. Add another component named Hero. Add a Short Text element to the Hero component and name it “Image alt”. Then create a custom component “Products” by clicking “Component” and “Create component”. Add a Short Text element named “Header” and a Reference named “Products list” referencing One-to-Many your product collection.

This is what the page structure will look like at the end:

undefined

Click “Save”, and after being redirected to another page, click the gear icon and “Configure Page Type”. Then you will be redirected again to a page where you should click “Create page type” and type the name for this page type.

Connect ButterCMS pages to the custom Storefront

Install the ButterCMS npm package to the Angular application by running the following command:

npm install buttercms


In the Angular application, create a module with routing and add a new component that will be used as a page template for the created page type. Update the module’s routing module by adding this route:

{
    path: ':locale/:slug',
    component: PromotionalPageComponent,
    resolve: {
      page: PromotionalPageResolverService
    },
  }

Change PromotionalPageComponent to the name of the component you just created. If your ButterCMS account does not have locales, then the path should be just :slug.

We have not yet created PromotionalPageResolverService that is used in this code. So let’s create it. This service should return page content retrieved from ButterCMS by slug and locale (if locales are set up). 

First, let’s create a service to interact with ButterCMS:

@Injectable({
  providedIn: 'root',
})
export class ButterCMSService {
  private butter = Butter(this.environmentVariablesService.butterCMSToken);
  private promotionalPageType = 'storefront';

  constructor(
    private environmentVariablesService: EnvironmentVariablesService
  ) {}

  getPromotionalPageData(
    slug: string,
    locale: string
  ): Observable<PromotionalPage> {
    return from(
      this.butter.page.retrieve(this.promotionalPageType, slug, { locale })
    ).pipe(
      map((response) => {
        return response.data.data;
      })
    );
  }
}

Set the promotionalPageType to the page type name that you created. (Note: Skip locale if they’re not set for your ButterCMS account.) PromotionalPage should match the created page type’s structure.

Now everything is ready for our resolver service:

@Injectable({
  providedIn: 'root',
})
export class PromotionalPageResolverService
  implements Resolve<PromotionalPage> {
  constructor(private butterCMSService: ButterCMSService) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<PromotionalPage> {
    const slug = route.paramMap.get('slug');
    const locale = route.paramMap.get('locale');
    return this.butterCMSService.getPromotionalPageData(slug, locale);
  }
}


Update the
PromotionalPageComponent component to get the resolved page content:

@Component({
  templateUrl: './promotional-page.component.html',
  styleUrls: ['./promotional-page.component.scss'],
})
export class PromotionalPageComponent implements OnInit {
  page: Observable<PromotionalPage>;

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    this.page = this.route.data.pipe(
      map((data: { page: PromotionalPage }) => {
        return data.page;
      })
    );
  }
}

In the component template, use page | async to display the page content.

That’s it! When we go to the page /pages/{slug} where {slug} is one of the slugs of your promotional pages, you should see the page's content that you set in ButterCMS.

Row 55.png

Summary

In this headless Shopify tutorial, we covered how to interact with the Shopify Storefront API to display store collection products, how to synchronize store products with ButterCMS collections, and how to integrate ButterCMS pages into the Shopify storefront. Now you can build your own unique branded promotional pages using ButterCMS!  The full code is available in the GitHub repository https://github.com/ButterCMS/headless-shopify, and a fully working demo is available at https://cranky-galileo-d83d7f.netlify.app/.

If you try this demo out, I'd love to hear your feedback and questions about this approach.  I would love to write more tutorials to support your headless Shopify projects and would love to hear down below in the comments what else you'd like to see!

Learn how to set up your headless commerce site by signing up for our monthly newsletter.
    
Orly Knop

Web developer who loves Angular 2+ and Node.js.

ButterCMS is the #1 rated Headless CMS

G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award

Don’t miss a single post

Get our latest articles, stay updated!