April 03, 2022

Build full stack e-commerce web app with React/Next.js , Apollo/GraphQL and Keystone Headless CMS - Part 1

In this tutorial series, we are going to get our hands dirty and build an e-commerce web app using React/Nextjs, Apollo/GraphQL and Keystone Headless CMS.

The we app will be ready to deploy with full features for authentication and the complete checkout flow with credit card payment.

Since this is the long tutorial, we will touch base on each individual tool/library when we reach to specific section to avoid getting exhausted at the start.

The app we are going to build is called buymine,the same one from the latest tutorial series of building full-stack serveless app with aws amplify , you can check it from my blog archive .

Featured Image

Let’s not waste any time and bootstrap our application. As mentioned before, we will be using Next.js, a react framework that helps us handle most of the common tasks for us like routing,lazy loading, code splitting, server side rendering, pre-rendering and more.

Create a folder called buymine that will host our frontend and backend projects . Make sure you have Node.js installed version 10.13 or later then run below command to bootstrap our frontend app.

npx create-next-app frontend

Once installed, navigate to newly created frontend folder and then start the project by running

npm run dev

From there, you should be able to see our app running on lolcahost:3000.

Now, let’s examine a pages folder in our newly created project. Next.js offers a file-system based routing, meaning when a new page is added inside pages folder, it will automatically be available as a route.

In Next.js, a page is a React Component exported from a .js, .jsx, .ts, or .tsx file in the pages directory. Each page is associated with a route based on its file name.

From above, let’s see how that works by creating a products page. Go ahead to pages folder and create a products.js file that will export a react component as below:-

const Products = () => {
  return <h1>Products Page</h1>;
};
export default Products;

Now, if we go to http://localhost:3000/products, we should be able to see our products page. Let’s leave it for now, and go back to our homepage, and edit index.js file as below;-.

import Head from "next/head";
import styles from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Buymine - your next shopping partner</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Welcome to Buymine</h1>
      </main>

      <footer className={styles.footer}>
        Copyright &copy; 2022. All rights reserved
      </footer>
    </div>
  );
}

Note that the page updated immediately, that’s because Next.js has enable already fast refresh, which is a feature from react that allows us to have fast hot reloading.

We will be using material ui components and icons, so go ahead and install beforehand.

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

Now let’s go ahead and create a components folder inside src directory, and inside it Header folder with two files Header.module.css and Header.js

In this app, we will be loading css using CSS modules. CSS Module will allow class names to be scoped locally in which when you import the class names in javascript, it will will generate unique class names. Next.js has a built-in support for css modules.

CSS Modules are an optional feature and are only enabled for files with the .module.css extension.

Header.module.css

.header {
  display: flex;
  width: 100%;
  background: #f7bf50;
  justify-content: space-between;
  align-items: center;
}
.header img.header__logo {
  width: 250px;
  object-fit: contain;
  cursor: pointer;
}
.header__search {
  display: flex;
  flex: 1;
}
.header_searchInput {
  width: 95%;
}

.header__searchIcon {
  background: black;
  color: white;
  padding: 5px;
}
.header__menu {
  display: flex;
}
.header__menuItem {
  display: flex;
  align-items: center;
  padding: 0px 10px;
  cursor: pointer;
}
.header__menuItemText {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.header__menuItemAvatar {
  font-size: 34px;
  color: #333;
}
.header__menuItemText {
  font-size: 14px;
}
.header__menuItemText span {
  color: #333;
  font-size: 12px;
}

Header.js

import { useState } from "react";
import SearchIcon from "@mui/icons-material/Search";
import PersonIcon from "@mui/icons-material/Person";
import Modal from "@mui/material/Modal";
import Box from "@mui/material/Box";
import styles from "./Header.module.css";

const modalStyle = {
  position: "absolute",
  top: "50%",
  left: "50%",
  transform: "translate(-50%, -50%)",
  width: 500,
  bgcolor: "background.paper",
  border: "2px solid #000",
  boxShadow: 24,
  p: 4,
};

const AppModal = ({ isOpen, onClose, component }) => {
  return (
    <Modal
      open={isOpen}
      onClose={onClose}
      aria-labelledby="modal-modal-title"
      aria-describedby="modal-modal-description"
    >
      <Box sx={modalStyle}>{component}</Box>
    </Modal>
  );
};

const Header = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  return (
    <div>
      <div className={styles.header}>
        <img
          src="https://codecotzbucket.s3.us-east-2.amazonaws.com/logo.png"
          alt="BuyMine Logo"
          className={styles.header__logo}
        />
        <div className={styles.header__search}>
          <input type="text" className="header_searchInput" />
          <SearchIcon className="header__searchIcon" />
        </div>

        <div className={styles.header__menu}>
          <div
            className={styles.header__menuItem}
            onClick={() => setIsModalOpen(true)}
          >
            <PersonIcon className={styles.header__menuItemAvatar} />
            <div className={styles.header__menuItemText}>
              <span> Sign In</span>
              Account
            </div>
          </div>
        </div>
      </div>
      <AppModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        component={<h1>Auth Component </h1>}
      />
    </div>
  );
};

export default Header;

From here, our web app should loook like below:-

Buymine Homescreen

Now,let’s leave the frontend here for now and start doing our backend. For our backend will be using keystonejs.

Keystone is a headless CMS that will allow us to create GraphQL API, giving us admin UI to manage modals and more. Once we set up the GraphQL API, we will then connect it to our frontend application via apollo client.

Let’s then booststrap our backend project. keystone uses create-keystone-app command. Inside our project folder so far we have our frontend app . Let’s add the backend by bootstrapping keystone app with below command;-

npx create-keystone-app backend

If everything went well, navigate to our newly created backend project then start our app as below:

npm run dev

If the frontend app is still running, you might encounter an port error because by default KeystoneJS also runs on port 3000.

To edit the port we will open keystone.ts file which is the main entry file for configuring Keystone. under config object add below :

server: {
      port: 5000,
    }

Now, the server should be able to start succesfully on http://localhost:5000/. Open the url for the first time should direct you to the screen to create first user as below:-

Keystonejs Init Screen

In order to access keystone admin UI, we need to have a user, so go ahead and create an admin user from the screen presented.

Once logged in to a new Keystone Admin UI, you will see a dashboard with users and posts items. These are called lists in keystone which is basically our data modal/schema.

Go ahead to schema.ts file and under Lists config option, let’s extract our schema in to separate files. Go ahead and create a schema folder inside our src directory and inside it a user.ts file as below:-

User.ts

import { list } from "@keystone-6/core";
import { text, password } from "@keystone-6/core/fields";

export const User = list({
  fields: {
    name: text({ validation: { isRequired: true } }),
    email: text({
      validation: { isRequired: true },
      isIndexed: "unique",
      isFilterable: true,
    }),
    password: password({ validation: { isRequired: true } }),
  },
});

Then create a Product Schema as below:

Product.ts

import { list } from "@keystone-6/core";
import { text } from "@keystone-6/core/fields";
export const Product = list({
  fields: {
    name: text({ validation: { isRequired: true } }),
    description: text({ ui: { displayMode: "textarea" } }),
  },
});

Now , go ahead and edit our schema.ts file to use our newly created schema. The schema.ts file should be as below:-

import { Lists } from ".keystone/types";
import { Product } from "./schema/Product";
import { User } from "./schema/User";

export const lists: Lists = {
  User,
  Product,
};

Now, if you visit our admin UI, we should be able to add products as well as users. Go ahead and play with it.

Now, let’s go on and edit the product schema to fit all our fields required to create the product.

To add image upload support we need @keystone-6/cloudinary, let’s go ahead and install it.

npm i @keystone-6/cloudinary

Then, let’s also install dotenv package that will allow us to load environment variables from a .env file into process.env.

npm i dotenv

Now add it to our schema as below

import { list } from "@keystone-6/core";
import { text, float, select } from "@keystone-6/core/fields";
import { cloudinaryImage } from "@keystone-6/cloudinary";
import "dotenv/config";

export const Product = list({
  fields: {
    name: text({ validation: { isRequired: true } }),
    description: text({ ui: { displayMode: "textarea" } }),
    price: float(),
    status: select({
      options: [
        { label: "Draft", value: "DRAFT" },
        { label: "Available", value: "AVAILABLE" },
        { label: "Unavailable", value: "UNAVAILABLE" },
      ],
      defaultValue: "AVAILABLE",
      ui: {
        displayMode: "segmented-control",
      },
    }),
    image: cloudinaryImage({
      cloudinary: {
        cloudName: process.env.CLOUDINARY_CLOUD_NAME ?? "",
        apiKey: process.env.CLOUDINARY_API_KEY ?? "",
        apiSecret: process.env.CLOUDINARY_API_SECRET ?? "",
        folder: process.env.CLOUDINARY_API_FOLDER ?? "",
      },
    }),
  },
});

To get CLOUDINARYCLOUDNAME and other values, go to https://cloudinary.com/, login or signup and after that you will see the values under dashboard menu. Go ahead and add the values in our .env file.

From here our schema are ready and GraphQL APIs can be accessed via http://localhost:5000/api/graphql, go ahead play with it.

That’s it for now, on the next part will go on connecting our frontend with backend using apollo client. Hope you learn something new abd don’t hesitate to reach out to me directly or via comments section incase of anything.


Robert Rutenge
Front-end Developer | ReactJS enthusiast | Problem Solver . Find me on twitter and Github