DEV Community

Cover image for How to Build an E-commerce Store with Sanity and Next.js
Enodi Audu
Enodi Audu

Posted on

How to Build an E-commerce Store with Sanity and Next.js

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. You can also review the code here

E-commerce Store

Table of Contents

  1. Step 1: Install Nextjs
  2. Step 2: Setup Chakra UI
  3. Step 3: Setup Sanity Studio
  4. Step 4: Update Content Schemas
  5. Step 5: Query Data using GROQ
  6. Step 6: Display Content in your Ecommerce App
  7. Step 7: Deploy Sanity Studio
  8. Step 8: Deploy to Vercel
  9. Step 9: Next Steps

1. Install Nextjs

Open a terminal and run this command:

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This should start a server on port 3000. Open your browser and go to 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 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
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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)',
  }
});
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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 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
Enter fullscreen mode Exit fullscreen mode

This will authenticate you using the sanity.io API.

Install Sanity Studio

Within your project directory, run this command:

npm create sanity@latest
Enter fullscreen mode Exit fullscreen mode

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**
Enter fullscreen mode Exit fullscreen mode

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

cd studio

npm run dev
Enter fullscreen mode Exit fullscreen mode

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

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

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" }]
    })
  ]
})
Enter fullscreen mode Exit fullscreen mode

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()
    })
  ]
})
Enter fullscreen mode Exit fullscreen mode

Update index.ts to look like this:

//studio/schemaTypes/index.ts

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

export const schemaTypes = [
  productType,
  categoryType
]

Enter fullscreen mode Exit fullscreen mode

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.

This is what it should now look like:

Sanity Studio with Schema

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

Products

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

5. Query Data using GROQ

GROQ (Graph-Relational Object Queries) 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;
Enter fullscreen mode Exit fullscreen mode

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

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}
  );
}

Enter fullscreen mode Exit fullscreen mode

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,
    }`
  );
}

Enter fullscreen mode Exit fullscreen mode

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,
};

Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode
//src/app/globals.css

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Please review this repository 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. You'll encounter a CORS error preventing your application from loading.

To resolve this issue, include 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

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. 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!

Top comments (0)