How to Build an E-commerce Store with Sanity and Next.js
Tags: sanity, cms, headless, nextjs
Published: 2024-07-16 11:01:24

Building an e-commerce store can be daunting, especially when dealing with loads of data that don’t change often but still need to be readily available and up-to-date.

That's where tools like Sanity and Next.js come in handy. Sanity is a powerful headless CMS that allows you to manage your content effortlessly, while Next.js is a React framework that makes it easy to create fast, dynamic web applications. Together, they offer a robust solution for building and maintaining an e-commerce store efficiently.

In this article, we’ll walk through the step-by-step process of setting up an e-commerce store using Sanity and Next.js. We’ll cover everything from configuring your backend in Sanity to creating a responsive frontend with Next.js. By the end, you'll have a solid foundation for building scalable and maintainable online stores with ease.

Here is a screenshot of what the e-commerce store will look like. You can also check out the live app [here](https://ecommerce-store-eight-green.vercel.app). You can also review the code [here](https://github.com/enodi/ecommerce-store)


![E-commerce Store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vokc0kp3s4m08ykhk42y.png)


**Table of Contents**

1. [Step 1: Install Nextjs](#1-install-nextjs)
2. [Step 2: Setup Chakra UI](#2-setup-chakra-ui)
3. [Step 3: Setup Sanity Studio](#3-setup-sanity-studio)
4. [Step 4: Update Content Schemas](#4-update-content-schemas)
5. [Step 5: Query Data using GROQ](#5-query-data-using-groq)
6. [Step 6: Display Content in your Ecommerce App](#6-display-content-in-your-ecommerce-app)
7. [Step 7: Deploy Sanity Studio](#7-deploy-sanity-studio)
8. [Step 8: Deploy to Vercel](#8-deploy-to-vercel)
9. [Step 9: Next Steps](#9-next-steps)


## 1. Install Nextjs
Open a terminal and run this command:

```
npx create-next-app@latest
```

This will install the latest version of Next.js.

We'll name our project `sanity-nextjs-ecommerce-store` but you can pick any name you like. We'll use the following options to set up our Next.js app.

```
✔ What is your project named? … nextjs-sanity-ecommerce-store
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … Yes
```

This will install the required dependencies including TailwindCSS which we will use to style our web app.

To see the application, run the commands below:

```
cd nextjs-sanity-ecommerce-store

npm run dev
```

This should start a server on port `3000`. Open your browser and go to [localhost:3000](http://localhost:3000) to see it in action.

##2. Setup Chakra UI
Chakra UI is a React component library that provides customizable, accessible, and reusable UI components to build responsive web applications.

Chakra UI offers ready-to-use, reusable components, and we will be using some of them in our application, like the Drawer and Button components.

In your root directory, run this command or follow [this documentation](https://v2.chakra-ui.com/getting-started/nextjs-app-guide) to set up Chakra in your Next.js project (App Router).

```
npm i @chakra-ui/react @chakra-ui/next-js @emotion/react @emotion/styled framer-motion
```

This will install all the necessary dependencies to run Chakra UI.

### Setup Provider
In the `src` directory, create a folder named `lib`, and inside it, create another folder named `chakra`. Then, create a file called `ChakraProvider.tsx` within the `chakra` folder.

Copy and paste this code into `ChakraProvider.tsx`.

```
//src/lib/chakra/ChakraProvider.tsx

"use client";
import { ChakraProvider as Provider } from "@chakra-ui/react";

import { theme } from "@/lib/chakra/theme";

export function ChakraProvider({ children }: { children: React.ReactNode }) {
  return <Provider theme={theme}>{children}</Provider>;
}
```

Create a file named `theme.ts` inside the `chakra` folder, then copy and paste this code into `theme.ts`.

```
//src/lib/chakra/theme.ts

import { extendTheme } from "@chakra-ui/react";

export const theme = extendTheme({
  fonts: {
    heading: 'var(--font-lato)',
    body: 'var(--font-lato)',
  }
});
```

Create another file named `fonts.ts` inside the `chakra` folder, then copy and paste this code into `fonts.ts`.

```
//src/lib/chakra/fonts.ts

import { Lato } from 'next/font/google'

const lato = Lato({
  weight: "400",
  subsets: ["latin"],
  variable: "--font-lato"
})

export const fonts = {
  lato
}

```

These files instruct Chakra to use the `Lato` font as the default font for the application.

### Use Chakra Provider
Navigate to the `layout.tsx` component within your `src/app` folder and use Chakra Provider within the layout component.

This is what it will look like:

```
//src/app/layout.tsx

import type { Metadata } from "next";

import "@/app/globals.css";
import { fonts } from "@/lib/chakra/fonts";
import { ChakraProvider } from "@/lib/chakra/ChakraProvider";

export const metadata: Metadata = {
  title: "Create Next App", // You can update this
  description: "Generated by create next app", // You can update this
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={fonts.lato.variable}>
      <body>
        <ChakraProvider>{children}</ChakraProvider>
      </body>
    </html>
  );
}

```

## 3. Setup Sanity Studio
Sanity Studio is Sanity's open-source UI tool for managing website content. It allows you to add, edit, and delete data like text, images etc within Sanity.

If you don't have a Sanity account, create one [here](https://www.sanity.io) and create a new project.

### Log into Sanity from your terminal
If you already have an account and have logged in previously from your terminal, you can skip this step. Otherwise, run this command in your terminal:

```
npx sanity login
```
This will authenticate you using the sanity.io API.

### Install Sanity Studio
Within your project directory, run this command:

```
npm create sanity@latest
```

If you created a project during your account setup, you can continue using that project or create a new one from your terminal. In this article, we'll use an existing project that was set up when creating a Sanity account. You can configure your project using the options provided below.

```
? Select project to use: **ecommerce-store**
? Select dataset to use: **production**
? Would you like to add configuration files for a Sanity project in this Next.js folder? **No**
? Project output path: /Users/USER/Desktop/nextjs-sanity-ecommerce-store/**studio**
? Select project template Clean project with no predefined schema types
? Do you want to use TypeScript? **Yes**
? Package manager to use for installing dependencies? **npm**
```

After completing the setup, Sanity Studio will be installed locally. To view the studio, run these commands:

```
cd studio

npm run dev
```
Navigate to `localhost:3333`. Log in using the same method you used to create your account, and you should see the studio running locally.

![Sanity Login](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rhrvospt04a4xjda6z5c.png)


## 4. Update Content Schemas
If you've successfully set up and logged in to Sanity Studio locally, you should see a display similar to the screenshot below:

![Sanity Studio with Schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/28ueljzx3s4qddyl4xn6.png)

Currently, our studio is empty because we haven't defined any schemas yet. Next, we'll define our schemas.

Schemas are like blueprints that outline how different types of content should be structured. They describe what fields each piece of content can have and what kind of information those fields can hold. This helps keep content organized and manageable.

For this e-commerce store, we create two schemas: one for `Product` and another for `Category`.

- `Product Schema`: This schema defines how a product is structured within our store.
- `Category Schema`: This schema defines how product categories are structured within our store.

### Product Schema
Create a file named `product.ts` inside the `schemaTypes` directory located within the `studio` directory.

Copy and paste the code below into `product.ts` file

```
//studio/schemaTypes/products.ts

import { defineType, defineField } from "sanity";

export const productType = defineType({
  title: "Product",
  name: "product",
  type: "document",
  fields: [
    defineField({
      title: "Product Name",
      name: "name",
      type: "string",
      validation: (Rule) => Rule.required()
    }),
    defineField({
      title: "Product Images",
      name: "images",
      type: "array",
      of: [
        {
          type: "image",
          fields: [
            {
              name: "alt",
              title: "Alt Text",
              type: "string",
            },
          ],
        },
      ],
    }),
    defineField({
      title: "Product Description",
      name: "description",
      type: "text",
      validation: (Rule) => Rule.required()
    }),
    defineField({
      title: "Product Slug",
      name: "slug",
      type: "slug",
      validation: (Rule) => Rule.required(),
      options: {
        source: "name"
      }
    }),
    defineField({
      title: "Product Price",
      name: "price",
      type: "number",
      validation: (Rule) => Rule.required()
    }),
    defineField({
      title: "Product Category",
      name: "category",
      type: "reference",
      to: [{ type: "category" }]
    })
  ]
})
```

### Category Schema
Create a file named `category.ts` inside the `schemaTypes` directory located within the `studio` directory.

Copy and paste the code below into `category.ts` file

```
//studio/schemaTypes/category.ts

import { defineType, defineField } from "sanity";

export const categoryType = defineType({
  title: "Category",
  name: "category",
  type: "document",
  fields: [
    defineField({
      title: "Category Name",
      name: "name",
      type: "string",
      validation: (Rule) => Rule.required()
    })
  ]
})
```

Update `index.ts` to look like this:

```
//studio/schemaTypes/index.ts

import { productType } from "./product"
import { categoryType } from "./category"

export const schemaTypes = [
  productType,
  categoryType
]

```
Each schema and field needs to include the `name`, and `type` properties. Here's a quick overview of each property's role:

- The **name** property serves as the identifier for referencing the schema in query language contexts. It must be unique to prevent schema conflicts.
- **Type** indicates the specific schema type being defined. Setting it to `document` instructs the studio to enable the creation of new documents.

### Visit your studio
Navigate to the `studio` directory from your terminal and run `npm run dev`.

Open your browser and go to [localhost:3333](http://localhost:3333). 

This is what it should now look like:

![Sanity Studio with Schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aucm8jk555xlv4d3sbda.png)

### Populate your Schemas
Begin by creating categories, followed by creating products within those categories. Create as many categories and products as you want.

This is what it should now look like:

![Categories](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ak74rue6gmllx3cse6ay.png)


![Products](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6yfgah6wlsalz2a4hl2.png)


Next, we will query the product and category data to use within the application.

## 5. Query Data using GROQ
[GROQ (Graph-Relational Object Queries)](https://www.sanity.io/docs/groq) is the query language developed by Sanity for querying structured content in their backend. It enables retrieval and manipulation of data stored in Sanity's content lake.

To query product and category data, we'll use a library named `@sanity/client`. This library offers methods for querying, creating, updating, and deleting documents in Sanity. It is designed for use in both server-side and client-side JavaScript applications.

In the `lib` directory, create a new directory named `sanity`, and within it, create a file called `client.ts`.

Then, copy and paste the code below into `client.ts`.

```
//src/lib/sanity/client.ts

import { createClient, type ClientConfig } from "@sanity/client";

const config: ClientConfig = {
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
  useCdn: false
};

const client = createClient(config);

export default client;
```

Replace `process.env.NEXT_PUBLIC_SANITY_PROJECT_ID` and `process.env.NEXT_PUBLIC_SANITY_DATASET` with the correct `projectId` and `dataset` values.

- `projectId`: This is your project's ID. You can obtain it by running the command `npx sanity projects list` in your terminal or by visiting the projects page in your Sanity account. See the screenshot below.
- `dataset`: This is the name of your project's dataset. By default, it is `production`, but if you create a new dataset, you should update it to reflect your dataset name.


![Project's ID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ushvdkn3yj82d78bc370.png)

Create 3 new files named `product-query.ts`, `category-query.ts`, and `types.ts`. Copy and paste the following code into it:

```
//src/lib/sanity/product-query.ts

import { groq } from "next-sanity";
import client from "./client";

export async function getProducts() {
  return client.fetch(
    groq`*[_type == "product"] {
      _id,
      "categoryName": category->name,
      description,
      name,
      price,
      "productImage": {"alt": images[0].alt, "imageUrl": images[0].asset->url},
      "slug": slug.current
    }`
  );
}

export async function getSelectedProducts(selectedCategory: string) {
  return client.fetch(
    groq`*[_type == "product" && category->name == $selectedCategory] {
      _id,
      "categoryName": category->name,
      description,
      name,
      price,
      "productImage": {"alt": images[0].alt, "imageUrl": images[0].asset->url},
      "slug": slug.current
    }`,
    {selectedCategory}
  );
}

```

The `getProducts` function retrieves all products, whereas the `getSelectedProducts` function fetches specific products based on the chosen category.

```
//src/lib/sanity/category-query.ts

import { groq } from "next-sanity";
import client from "./client";

export async function getCategories() {
  return client.fetch(
    groq`*[_type == "category"] {
      _id,
      name,
    }`
  );
}

```
The `getCategories` function retrieves all categories.

You'll notice that the GROQ query begins with an asterisk (*), representing all documents in your dataset, followed by a filter in brackets. The filter used here returns documents with a `_type` of "product" or "category".

```
//src/lib/sanity/types.ts

export type ProductType = {
  _id: string,
  name: string,
  productImage: {
    alt: string,
    imageUrl: string
  },
  slu: string,
  categoryName: string,
  description: string,
  price: number,
};

export type CategoryType = {
  _id: string,
  name: string,
};

```
The `ProductType` and `CategoryType` define the structure of the `Product` and `Category` objects.

## 6. Display Content in your E-commerce App
It's now time for us to display the content within the e-commerce application.

Begin by stripping all styles from the `globals.css` file, leaving only essential Tailwind imports at the beginning. Next, erase the contents of your root `page.tsx` file in your Next.js application and replace it with the following code:

```
//src/app/page.tsx

"use client";

import { useDisclosure } from "@chakra-ui/react";
import { Fragment, useEffect, useState } from "react";

import Hero from "@/app/components/hero/Hero";
import Cart from "@/app/components/cart/Cart";
import Navbar from "@/app/components/navbar/Navbar";
import Products from "@/app/components/products/Products";
import { getCategories } from "@/lib/sanity/category-query";
import { ProductType, CategoryType } from "@/lib/sanity/types";
import { getProducts, getSelectedProducts } from "@/lib/sanity/product-query";

export default function Home() {
  const { isOpen, onOpen, onClose } = useDisclosure();
  const [products, setProducts] = useState<ProductType[]>([]);
  const [categories, setCategories] = useState<CategoryType[]>([]);
  const [selectedCategory, setSelectedCategory] = useState<string>("");

  const [cartItems, setCartItems] = useState<ProductType[]>([]);
  const [cartItemsCount, setCartItemsCount] = useState<number>(0);

  const localStorageCartItem =
    typeof window !== "undefined" && localStorage.getItem("cart");
  const parsedCartItems =
    localStorageCartItem && JSON.parse(localStorageCartItem);
  const itemsInCart = cartItems.length > 0 ? cartItems : parsedCartItems;

  const localStorageCartItemCount =
    typeof window !== "undefined" && localStorage.getItem("cartCount");
  const cartCount: number =
    localStorageCartItemCount && JSON.parse(localStorageCartItemCount);
  const itemCount = cartItemsCount || cartCount;

  useEffect(() => {
    async function fetchProducts() {
      const allProducts: ProductType[] = await getProducts();
      setProducts(allProducts);
    }
    fetchProducts();
  }, []);

  useEffect(() => {
    async function fetchCategories() {
      const allCategories: CategoryType[] = await getCategories();
      setCategories(allCategories);
    }
    fetchCategories();
  }, []);

  const handleDrawerOpen = () => onOpen();

  const handleProductFilter = async (category: string) => {
    let product: ProductType[] = [];
    if (!!category) {
      product = await getSelectedProducts(category);
    } else {
      product = await getProducts();
    }
    setProducts(product);
    setSelectedCategory(category);
  };

  const addCartItem = (product: ProductType) => {
    let cart: ProductType[] = [];
    const count = cartCount + 1;
    const products = [];
    products.push(product);

    if (!!itemsInCart) {
      cart = [...itemsInCart, ...products];
    } else {
      cart = [...products];
    }

    setCartItems(cart);
    setCartItemsCount(count);

    updateLocalStorage(count, cart);
  };

  const removeItemFromCart = (product: ProductType) => {
    const count = cartCount - 1;
    const filteredItems = itemsInCart.filter(
      (item: ProductType) => item._id !== product._id
    );

    setCartItems(filteredItems);
    setCartItemsCount(count);

    updateLocalStorage(count, filteredItems);
  };

  const updateLocalStorage = (count: number, cart: ProductType[]) => {
    if (typeof window !== "undefined") {
      localStorage.setItem("cartCount", JSON.stringify(count));
      localStorage.setItem("cart", JSON.stringify(cart));
    }
  };

  return (
    <Fragment>
      <Navbar handleDrawerOpen={handleDrawerOpen} itemCount={itemCount} />
      <main>
        <Hero
          categories={categories}
          handleProductFilter={handleProductFilter}
        />
        <Products
          products={products}
          selectedCategory={selectedCategory}
          addCartItem={addCartItem}
        />
      </main>
      <Cart
        isOpen={isOpen}
        onClose={onClose}
        itemsInCart={itemsInCart}
        removeItemFromCart={removeItemFromCart}
      />
    </Fragment>
  );
}

```

```
//src/app/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;
```

Please review [this repository](https://github.com/enodi/ecommerce-store) for additional components like `Navbar`, `Hero`, `Products`, and `Cart` used in `page.tsx`.

### Add `localhost:3000` to CORS Origin
After setting up your frontend application and including all necessary components, launch your Next.js application with `npm run dev`.

This will start your Next.js app on port `3000`. Open your browser and navigate to [localhost:3000](http://localhost:3000). You'll encounter a CORS error preventing your application from loading.

To resolve this issue, include [localhost:3000](http://localhost:3000) to the list of allowed hosts that can connect to your project's API.

You can achieve this via the terminal using `npx sanity cors add http://localhost:3000`, or by logging into your Sanity account and adding `localhost:3000` to the CORS origin list. Refer to the screenshot below for guidance.

![CORS Origin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eu5ca8ty201immufmin2.png)

Now, restart your application, and you should see a list of products that were added through your Sanity Studio.

You have the option to add products to your cart, remove them, and filter the product list by category.


## 7. Deploy Sanity Studio
Once your application is up and running locally, you can synchronize your schemas with your remote Sanity Studio by running `npx sanity deploy`. This ensures that your remote studio reflects the latest changes made locally.


## 8. Deploy to Vercel
To make your e-commerce store accessible online, deploy it using [Vercel](https://vercel.com). Vercel provides seamless deployment for Next.js applications, ensuring your site is fast and reliable. Simply link your GitHub repository to Vercel and trigger automatic deployments with every push to your main branch. Once deployed, your store will be live and accessible to users worldwide.

## 9. Next Steps
While this tutorial has provided a solid foundation, there are numerous ways to further enhance your e-commerce store using Sanity.

Thank you for reading! Remember to like, share, and follow for future updates and more insightful content. Until next time, happy coding!