Next.js E-Commerce Tutorial: SPA Example

In a rush? Skip to the tutorial or live example

Like many web developers, I have been doing quite a lot of React stuff recently. The last few years have seen its usage in the industry grow in a trajectory commensurate to its parent company.

These days, there isn’t much you can’t do with React, whether you’re a seasoned developer or a complete beginner.

This is mostly due to the creation of tools such as Next.js that have successfully simplified React frontend development.

So, today, we'll explore how to craft a Next.js e-commerce single-page application quickly.

In the technical tutorial below, I’ll show you how to:

  • Set up a Next.js development environment

  • Create new pages & components

  • Fetch data & import components

  • Create serverless API routes in Next

  • Add a shopping cart to a Next.js app

  • Style the app

But before we go through this, let’s make sure we understand what Next.js is and how it can improve your next e-commerce projects.

What's Next.js?

In a nutshell, Next.js is a lightweight framework for React applications that allows you to build server-side rendering and static applications in React easily.

It takes all the good parts of React and makes it even easier to get an app running with optimized rendering performance. Next.js does this thanks to multiple built-in configurations—automatic code-splitting, file-system routing, server-side rendering, static files exporting, and styling solutions.

Trust me when I say that you can build a LOT of different things with Next.js:

It was built by Zeit (now Vercel) back in 2016 and has quickly gained traction to the point of becoming one of the most popular tools of its kind. I mean, it’s used by Marvel, Netflix, Uber, Nike… and the list goes on.

Okay, this is all great, and I’m genuinely excited to play with Next.js here. But is it any good for e-commerce?

Next.js & e-commerce: a good fit?

Like any static site generator or JavaScript framework out there, one of its most significant advantages, vs. more traditional e-commerce platforms, is the options it gives to developers to create a kickass shopping UX while removing the burden of many implementation details required for building a web app.

Depending on your need, you can easily build a server-side or statically rendering app with Next, which implements those for you while also abstracting other details such as app-bundling and transcompilation.

The power of the Jamstack right here!

We’ve covered the general React e-commerce ecosystem and its benefits in an earlier post. I would strongly suggest reading it to understand further why it’s a great fit.

But on the probable chance that you're pressed for time, here’s a TL;DR:

→ The use of components for flexibility.

Component-based development enables easy code reuse through your app but also the writing of small features. Or, in our case, small e-commerce functionalities. This comes in handy once you start scaling and expanding your shopping cart integration.

→ Virtual DOM (document object model) for performance.

React’s virtual DOM provides a more efficient way of updating the view in a web application. Performance is Everything in e-commerce; all milli-seconds count.

→ Popularity & vast community.

Any issue has probably been documented already, so you're likely to find a solution to any potential pitfalls in your way.

Next.js features like server-side rendering and static exporting push these React benefits even further by guaranteeing that your website/app will be SEO-friendly. This is something vital to any e-commerce business.

Still need social proof? Here are some Next.js e-commerce site examples.

Technical tutorial: a Next.js e-commerce SPA

Okay, time to jump into code and create our own handcrafted Next.js e-commerce app with the help of Snipcart. For you fish aficionados — or really anyone waiting for the beta of any cool software library — rest assured because we will make a betta fish store today.

Pre-requisites

Basic knowledge of React & TypeScript will also help you here, but it is not mandatory to follow along.

1. Setting up the development environment

First, let set up our environment so that we can start building.

Open a terminal and type the following command: npx create-next-app --typescript

A prompt will appear asking you for the project's name. It will then install all of the project dependencies for you and create files and folders. We'll look into these further in this tutorial.

Beginner note: The command's --typescript options will set up a Typescript project, which I often favor. The typing system helps prevent many runtime errors and perky bugs. It also allows for better refactoring in the long run!

Then, run npm run dev. Your app should now be served at localhost:3000.

2. Defining a layout

With our environment ready, let's create a layout for our store. It will include a Header and a Footer with links to our cart and contact information.

We will add this layout to the app's main entry point. In Next, this entry point is located at pages/_app.tsx. You can see that the MyApp function returns the pageProps. We will use this function to create our app's layout:

At the project's root, create a components directory in which — you guessed it — we will create our components.

1. Creating components

Now let's create the components we need.

In the components directory, create a Header.tsx file with the following content:

// components/Header.tsx
import Link from "next/link";

export default function Header() {
    return (
        <header >
            <Link href="/">
                <img src="/static/logo.svg" alt="" >
            </Link>
            <Link href="/">
                <h1 >FishCastle</h1>
            </Link>
            <a href="#" style={{textDecoration: "none"}}>
                <svg width="31" height="27" viewBox="0 0 31 27" fill="none" xmlns="<http://www.w3.org/2000/svg>">
                    <path d="" fill="#9094FF"/>
                </svg>
                <span></span>
            </a>
        </header>
    )
}

The Link component from Next.js allows us to convert most HTML elements into in-website links.

Still in the components directory, create a Footer.tsx file with the following content:

// components/Footer.tsx

export default function Footer(){
    return (
        <footer>
            <p>
                Next.js app with a&nbsp;<a href="<https://snipcart.com>">Snipcar        t</a>&nbsp;- powered store
                <div >
                    <a href="<https://github.com/snipcart/snipcart-nextjs>-spa">Github</a>
                </div>
            </p>
        </footer>
    )
}

2. Integrating components

Let's now integrate those components into our app. First, create a Layout component and put the Header and Footer in it:

import Header from "./Header";
import Footer from "./Footer";
import {PropsWithChildren} from "react";

export default function Layout({ children  }: PropsWithChildren<any>) {
  return (
      <>
          <Header />
          <main>{children}</main>
          <Footer />
      </>
  )
}

With your layout components created, all that's left to do is to add it to the _app.tsx file:

// _app.tsx
function MyApp({ Component, pageProps }: AppProps) {
  return <>
    <Layout>
      <Component {...pageProps} />
    </Layout>
    </>
}

If you run your app's dev mode and go to your localhost page, you should now see your app's layout created. Later in this tutorial, we will see how to add style to it and give it that drip.

But first things first, let's give our homepage the content it deserves.

3. Customizing your homepage

As we need to display both information about our store and the products we will sell, we will create a few different components to keep things modular and maintainable. Then, we will look at how to assemble them:

2. Creating required components

The product component

As this is a Next.js tutorial for an e-commerce app, you will need a Product component to show on your homepage.

The component will output whatever information you need to display about our particular product. You can create an IProduct interface that matches Snipcart's product definition and an IProductProps interface to define the types of our props, which will be pass as a parameter to the function.

// components/Product.tsx

export interface IProduct {
    id: string
    name: string
    price: number
    url: string
    description: string
    image: StaticImageData
}

Underneath the interface, add this component:

// components/Product.tsx

interface IProductProps {
    product: IProduct
}

const Product = (props: IProductProps) => {
    return (
        <div className={styles.product}>
            <h2 className={styles.product__title}>{props.product.name}</h2>
            <p className={styles.product__description}>{props.product.description}</p>
            <div className={styles.product__image}>
            <Image src={props.product.image} alt={props.product.image.src} />
            </div>
            <div className="product__price-button-container">
                <div className={styles.product__price}>${props.product.price.toFixed(2)}</div>
                <button
                    className={`snipcart-add-item ${styles.product__button}`}
                    data-item-id={props.product.id}
                    data-item-name={props.product.name}
                    data-item-price={props.product.price}
                    data-item-url={props.product.url}
                    data-item-image={props.product.image.src}>
                    Add to cart
                </button>
            </div>
        </div>
    )
}

A note on the Image component

Notice in the block below we are using Next.js's Image component instead of a good ol' img tag. The former is actually an extension of the latter. It allows automatic image optimization, lazy loading by default, and providing images in WebP when the browser allows it, which optimizes images to the client device. Moreover, the component optimizes image on requests, which saves you build time. This helps to reduce your website loading time and thus keep your users' interest!

3. The product list component

We will integrate this product component into a ProductList component, whose name is quite self-explanatory. The ProductList.tsx component will be used to display our list of products on the homepage. Therefore, you can create an IProductListProps interface that describes an array of IProduct, which is eventually going to be passed by our website:

import Product, {IProduct} from "./Product";

interface IProductListProps {
    products: IProduct[]
}

const ProductList = (props: IProductListProps) => {
    return (
        <div className="product-list">
            {props.products.map((product, index) => <Product product={product} key={index}/>)}
        </div>
    )
}

export default ProductList

4. Pre-rendering data and importing components

At this stage, you'll probably want to populate your products to the ProductList component. In pure React, you could use React's useEffect lifecycle inside the ProductList to fill the data. However, this method won't get called on the server during a static or server-side rendering.

Thankfully Next.js adds two ways to pre-render the data: getStaticProps, which fetches data at build time, and getServerSideProps, which fetches data on each request. The latter may be useful, for example, for an auction store where the price may be rapidly fluctuating. In our use case, since the product doesn't change often, we will use the former as the pre-rendering will decrease loading time by saving the user a request.

<main className="main">
    <Jumbotron />
    <ProductList products={products}/>
    <Contact/>
</main>

export const products: IProduct[] = [
    {
        id: "halfmoon",
        name: "Halfmoon Betta",
        price: 25.00,
        image: halfmoonBettaPicture,
        description: "The Halfmoon betta is arguably one of the prettiest betta species. It is recognized by its large tail that can flare up to 180 degrees.",
        url: '/api/products/halfmoon'
    },
    (...)
    {
        id: "veiltail",
        name: "Veiltail Betta",
        price: 5.00,
        image: veiltailBettaPicture,
        description: "By far the most common betta fish. You can recognize it by its long tail aiming downwards.",
        url: '/api/products/veiltail'
    }
]

export const getStaticProps: GetStaticProps = async (context) => {
    return {
        props: {
            products
        }
    }
}

Jumbotron & Contact components are added to complete the page, but they are not essential for the demo. You can view the components in the GitHub repo here.

5. Importing Snipcart

Now, let's install Snipcart into our website. First, you'll need to import the Head component from next/head inside your index.tsx page, which will allow you to add HTML inside the <Head> element.

You can do so by adding the following code inside the Index function return clause:

// pages/index.tsx
<Head>
    <title>My awesome store</title>
    <link rel="preconnect" href="<https://app.snipcart.com>"/>
    <link rel="preconnect" href="<https://cdn.snipcart.com>"/>
    <link rel="stylesheet" href="<https://cdn.snipcart.com/themes/v3.2.0/default/snipcart.css>"/>
    <link rel="shortcut icon" href="../public/favicon.ico" />
</Head>

For more information on how to use the Head component to optimize for SEO, have a look at our article regarding SEO using Next.js.

We now need to load Snipcart's script content. Next.js offers a Script component in the next/script, module to do so. It allows for performance optimization by offering different loading strategies.

// pages/index.tsx
<script src="https://cdn.snipcart.com/themes/v3.2.0/default/snipcart.js"></script>
<div hidden id="snipcart" data-api-key="YOUR_PUBLIC_API_KEY"></div>

Don't forget to swap the data-api-key attribute with your own API key ;)

6. Product validation

Now that Snipcart is installed, the last step before completing orders is to validate your products.

1. HTML validation

The first way to do that is by simply changing the URL in your product list to / for each product to the homepage for Snipcart's HTML validation. It will read the / on our homepage and crawl it in order to validate the products if you want. You can do just that and skip to the next section, and you will have a working e-commerce site!

If you are curious, let's take the opportunity to check out a neat Next.js feature: serverless API routes combined with Snipcart's JSON validation.

2. JSON validation using Next.js serverless API

For more complex scenarios, it might be useful to have an API returning our products information in JSON format. To do so, we will need to have a unique URL for each product that will return its information in a JSON file.

  1. Configuring static API routes

While technically we only need a dynamic API route returning each product, let's make this API RESTful and have a route returning the whole product list.

You may have noticed that an API folder was created with the project. In this folder, create another one called products and add an index.ts file to it with the following content:

// In pages/api/products/index.ts

import {NextApiRequest, NextApiResponse} from "next";
import {products} from "../../index";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json(products);
}

If you now go to https://localhost:3000/${YOUR_PORT}, you will get a JSON file containing your product list.

  1. Configuring dynamic API routes

In the products folder, add the following to the [productId].ts file. Notice the brackets. This special syntax tells Next.js that [productid] is a dynamic parameter. Hence if you go to /api/products/ONE_OF_YOUR_PRODUCTS_ID, you should get the JSON information of one of our products.

import {NextApiRequest, NextApiResponse} from "next";
import {products} from "../../index";
import {IProduct} from "../../../components/Product";

export interface ISnipcartProduct {
    id: string
    name: string
    price: number
    url: string
    description: string
    image: string // Hack to pass the image URL instead of the StaticImage object we required
}

export default function handler(req: NextApiRequest, res: NextApiResponse) {
    const {productId} = req.query;
    const product: IProduct | undefined = products.find(p => p.id === productId);
    if (!product) {
        res.status(404).json({});
        return ;
    }
    const snipcartProduct: ISnipcartProduct = {...product, image: product?.image.src ?? ""}

    res.status(200).json(snipcartProduct);
}

You should now be able to complete a test order!

It's time to style our website, so it's more appealing to our future customers.

7. Styling your Next.js SPA

If you paid attention, you saw that most of the components in this tutorial already had classnames. We will now look at 2 different ways to apply them:

1. Setting up a global stylesheet

In the styles folder, create a global.scss style sheet. Afterwards, simply import it to pages/_app.tsx:

// in pages/_app.tsx

import "../styles/globals.scss";

Global stylesheets can only be imported in the _app.tsx file. I used SCSS, which does not come built-in with Next.js, but can be easily integrated simply by running npm install sass.

2. Setting up CSS modules for our components

Next.js also supports CSS modules, which can become quite handy if your CSS file gets larger. To use it, simply create a file respecting the [name].module.css convention, for example, Product.module.css or Product.module.scss.

Afterward, you can import it as a styles object in the component's file and access the styles with it:

import styles from '../styles/Product.module.scss';
(...)

const Product = (props: IProductProps) => {
  return (
      <div className={styles.product}>
          <h2 className={styles.product__title}>{props.product.name}</h2>
          <p className={styles.product__description}>{props.product.description}</p>
      (...)
  )
}

For further examples of how these styles are applied, have a look at the project:

Allow me a shout-out to Michael from Snipcart, who came up with the neat UI design!

And voilà! You're server-side rendered Next.js e-commerce store should be ready to go.

Closing thoughts

I liked how easy it was to create a static website with great performance using Next.js. I did notice some parts of the Next.js documentation could be more up-to-date.

We could have explored Image optimization on mobile using the Image component or Next's dynamic imports to push this demo further.

Are you up to it? If so, let us know how it goes in the comments below!


Liked this article? Hit the share buttons below.

About the author

Pierre-Guillaume Laurin
Developer

Pierre has been programming for over 5 years now. His go-to languages are Javascript and Python. It doesn’t stop him from getting into other technologies such as Haskell, which he currently is avidly learning. Also a UX enthusiast, he loves to build intuitive products and interfaces that best fit users’ needs.

A Guide to Node.js for E-Commerce [With Koa.js Tutorial]

Read next from Pierre-Guillaume
View more

36 000+ geeks are getting our monthly newsletter: join them!