Building a Clothing Store App

Milecia

When you're building an online shopping platform, you want to make sure that everything loads quickly for your potential customers. One huge factor that slows down many e-commerce sites is loading images. When you have hundreds of product images loading on a page at the same time, it can create a frustrating user experience as people wait to see how things look.

In this article, we're going to build a simple clothing store to demonstrate how you can make fast-loading pages. We'll be working with Next.js for the front-end and back-end of the app and we'll be using Cloudinary as the image host.

Setting up the Next app

Let's start by generating a Next.js app. Open your terminal and run this command. It'll generate all of the files you need for a Typescript Next app.

1$ yarn create next-app --typescript

At some point, you'll be prompted to enter the name of the project. The one we'll be working with is called tienda-de-ropa, but feel free to name it anything you like. After you set the name, the downloads will start and the project files will be created.

Most of our work will be in the pages folder because this is where we'll add the pages that users will interact with on the online store.

Let's install a few packages before we get too far ahead of ourselves.

1$ yarn add typescript ts-node @types/node @prisma/client prisma styled-components

Setting up Prisma and a Postgres database

Before we get to that though, we're going to be pulling items from a Postgres database and using Prisma to do so. If you don't have Postgres installed locally, go ahead and download it here and get set up. You should create a new database called tienda-de-ropa so that we can connect to it immediately.

Now we need to get Prisma set up in our app. We'll start by adding a new file to the root of the project called prisma.ts. This will handle the Prisma client we need to interact with and keep our app from having issues due to database connection limits. Inside of this file, add the following code:

1import { PrismaClient } from "@prisma/client";
2
3let prisma: PrismaClient
4
5if (process.env.NODE_ENV === 'production') {
6 prisma = new PrismaClient()
7} else {
8 if (!global.prisma) {
9 global.prisma = new PrismaClient()
10 }
11 prisma = global.prisma
12}
13export default prisma

That's not the only new thing we'll be adding to this project to get Prisma working. In the root directory, add a new folder called prisma. Inside the new sub-folder, add a file named schema.prisma. This will hold the database schema and the instructions that tell Prisma how to connect to Postgres. Open this file and add the following code:

1generator client {
2 provider = "prisma-client-js"
3}
4
5datasource db {
6 provider = "postgresql"
7 url = "postgres://usernamer:password@localhost:5432/tienda-de-ropa"
8}

This code defines how Prisma connects to Postgres by defining the database provider type and the connection string. Make sure to update the username and password to your own values. Since we have the connection established, it's time to create the model for our products. Below the code we just wrote, add this model definition:

1model Product {
2 id String @id @default(uuid())
3 name String
4 category String
5 price Float
6 image String
7 description String
8}

The model has a few fields that are commonly seen in online stores, but feel free to add any other fields you like! We're going to take advantage of NextJS doing server-side rendering and not worry about building a back-end. You can extend this app and add some back-end functionality with NextJS API routes, but that'll be out of the scope of this post.

To have some product data to work with, we'll seed the database with a few different products.

Seeding the database

Create a new file in the prisma directory called seed.ts. This is the standard file name you'll see when working with Prisma seed data. In this file, we'll add the following code and then discuss what's happening:

1import { PrismaClient, Prisma } from '@prisma/client'
2
3const prisma = new PrismaClient()
4
5const products: Prisma.ProductCreateInput[] = [
6 {
7 name: 'Blue Shirt',
8 category: 'Tops',
9 price: 29.99,
10 image: 'https://res.cloudinary.com/milecia/image/upload/v1606580778/3dogs.jpg',
11 description: 'Blue shirt with dog print'
12 },
13 {
14 name: 'Taupe Pants',
15 category: 'Bottoms',
16 price: 59.99,
17 image: 'https://res.cloudinary.com/milecia/image/upload/v1606580780/beach-boat.jpg',
18 description: 'Ankle length pants'
19 },
20 {
21 name: 'Black Patent Leather Oxfords',
22 category: 'Shoes',
23 price: 99.99,
24 image: 'https://res.cloudinary.com/milecia/image/upload/v1606580772/dessert.jpg',
25 description: 'Ankle high dress shoes'
26 }
27]
28
29async function main() {
30 console.log(`Start seeding ...`)
31 for (const p of products) {
32 const product = await prisma.product.create({
33 data: p,
34 })
35 console.log(`Created product with id: ${product.id}`)
36 }
37 console.log(`Seeding finished.`)
38}
39
40main()
41 .catch((e) => {
42 console.error(e)
43 process.exit(1)
44 })
45 .finally(async () => {
46 await prisma.$disconnect()
47 })

We start off by importing a few objects from the Prisma client package and setting up an instance of the client. Then we create a typed array for the products we want to add to the database. If you have images in Cloudinary already, you should definitely replace these example images with your own.

Then we have a main function that will add all of these products to the Product table we defined in our schema file. Finally, we'll call the main function to execute the seeding and then disconnect from the database if there are no errors. That's all the code we need for seeding this data.

There's one more tiny thing we need to do before we can run a migration to put the schema and seed data in the tienda-de-ropa database. Since this is a Typescript project, that means we need to be more explicit about how the app should handle seeding the data in the migration.

We need to add a small script to the package.json so everything runs smoothly. Open this file and add the following code below your devDependencies:

1"prisma": {
2 "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
3}

This will help us avoid any errors due to Typescript interfacing with regular JavaScript packages. Now we can run the migration. Open your terminal and run the following command:

1$ npx prisma migrate dev

You will be prompted for a name for this migration and then it will create the database schema in Postgres and add your seed data to it. Now that we have the data side of the app set up, we can jump over to the front-end and get a few pages up.

Making the product listing page

Like most online stores, we're going to have a few pages. There will be a page that displays all of the products, individual product pages, and a cart that shows customers all of the items they've selected. Keep in mind that this is just a demo so there's a lot of functionality, styles, and other considerations that could be added.

Let's start by making a page that displays all of the products. Adding a new route in NextJS means adding a new file to the pages directory. So we're going to add a file called products. Open this new file and add the following code.

1import Image from 'next/image';
2import Link from 'next/link'
3import styled from 'styled-components'
4import prisma from '../prisma'
5
6interface Product {
7 id: string
8 name: string
9 category: string
10 price: number
11 image: string
12 description: string
13}
14
15function Products() {
16 return <div>Products</div>
17}
18
19export default Products

We've added all of the imports we need for this specific file and we've defined the type for the product data we'll receive from the database. We've also added the Products component that will get rendered on the page. If you run the app now with yarn dev, you'll just see the "Products" text on the page.

Next let's add a few styled components so that the app has a bit of organization. Right below the product type definition, add this code:

1const HoverCard = styled.div`
2 margin: 12px;
3 padding: 12px;
4 width: 30%;
5
6 &:hover {
7 border: 1px solid #c4c4c4;
8 cursor: pointer;
9 }
10`
11
12const ToCart = styled.div`
13 border: 1px solid #c4c4c4;
14 background-color: #abacab;
15 width: 150px;
16`

The HoverCard element will be used to display some overview info about each product and if you click on one of them, you'll be redirected to the individual product page that has all of the details about that product. The ToCart element will wrap the link to the current user cart so that it looks more button-like instead of like a link. We do this because NextJS has a specific element to handle links.

Getting data using the getStaticProps function

NextJS does a great job at server-side rendering, so that means we don't have to worry about sensitive info leaking even though we are going to fetch data directly from the database. Any code defined in the getStaticProps function is executed at build time on the server-side.

To get all of the products from the database, add the following code just above the export statement line:

1export async function getStaticProps() {
2 const products = await prisma.product.findMany()
3
4 return {
5 props: {
6 products,
7 },
8 }
9}

This code lets us connect to the database directly through our Prisma client and we get all of the product records available. Then we return this product data as a prop to the Products component. So let's start filling in the code for that component now that we have the data.

Wrapping up the products page

With our data readily available, let's use it to display the shop's products to potential customers. Update your Products component to match the following code:

1function Products({ products }) {
2 return (
3 <div style={{ margin: '24px' }}>
4 <h1>Everything in the store</h1>
5 <div style={{ display: 'flex' }}>
6 {
7 products.map((product: Product) => (
8 <Link
9 passHref
10 key={product.name}
11 href={`/product/${encodeURIComponent(product.id)}`}
12 >
13 <HoverCard>
14 <Image src={product.image} alt={product.description} height={250} width={250} />
15 <h2>{product.name}</h2>
16 <p>{product.category}</p>
17 </HoverCard>
18 </Link>
19 ))
20 }
21 </div>
22 <Link passHref href={`/cart`}>
23 <ToCart>Go to Cart</ToCart>
24 </Link>
25 </div>
26 )
27}

Thankfully, this component is relatively simple because it only has one set of data to work with and the elements are already defined for us. There's a styled div that wraps everything and we have a small heading for the page. Then we get to the most important part of this component, where we actually display the data we pulled from the database.

In the div with the flex style, we take the products prop that was passed in from the getStaticProps function and map it to the different elements. First, we have a link that routes to the individual product pages based on the product id.

Within that Link, we display the product image, name, and category so that customers can quickly find what they need. The last element we have is a link to the cart so that customers can see what they've decided to purchase. Take a moment to run the app and navigate to the /products route. You should see something similar to this:

Now the customers can see all of the products, we need to work on the individual product pages.

Adding the product page

NextJS uses the folder structure in the pages directory to determine how to handle nested routes. To make the individual product pages, we'll use the product id as the differentiator. Let's add a couple of new folders and a file to the pages directory.

Start by adding a folder named product. This will create a new route like /product. Inside of this folder, add another folder named [id]. This is how NextJS does dynamic routing. So this gives us routes like, /product/[id] where id is the product id getting passed from the products page.

Inside the [id] folder, create a new file called index.tsx. This is where we'll make the component for a single product. Go ahead and open this file and add the following code:

1import Image from 'next/image'
2import Link from 'next/link'
3import styled from 'styled-components'
4import prisma from '../../../prisma'
5
6const Back = styled.div`
7 border: 1px solid #c4c4c4;
8 background-color: #abacab;
9 width: 150px;
10`
11
12function Product() {
13 return ()
14}
15
16export default Product

We're bringing in the imports like normal and making another styled component to wrap the link back to the products page and we have the outline of the Product component we'll display on the page.

Now we need to fetch the data for a particular product based on the id parameter that's passed in the URL.

Retrieving data with getServerSideProps

Since this data will need to be retrieved on each request because there will be different products being displayed, we'll use the getServerSideProps function instead of the getStaticProps function.

All of the data fetching will still happen server-side, it will just happen each time this route is requested. So just above the export statement, add the following code:

1export async function getServerSideProps(context) {
2 const id = context.params.id
3
4 const product = await prisma.product.findUnique({
5 where: { id }
6 })
7
8 return {
9 props: {
10 product,
11 },
12 }
13}

Because this page uses dynamic routing, we are using the context parameter to get the params from the route parameters. So since our page is in the [id] folder, we'll be able to get the product id from this context object. Then we make a call to the database using the Prisma client to get the specific product based on its id and we pass the product data to the component as a prop.

Filling in the product component

This component will render the image, name, price, and category for the specified product and there will be a button to add the item to a customer's cart. There are a number of ways to manage cart items, but if you take a look in the developer tools on your favorite store's website, you'll likely see an array is stored in the local storage. We'll take that approach here.

Let's start by adding a function inside of the Product component that handles products being added to the cart. Update your existing Product component with the following code:

1function Product({ product }) {
2 function addToCart() {
3 let arr = []
4
5 const existingItems = window.localStorage.getItem('lineItems')
6
7 if (existingItems != null) {
8 arr = JSON.parse(existingItems)
9 }
10
11 const itemForCart = {
12 name: product.name,
13 price: product.price,
14 imageUrl: product.image
15 }
16
17 arr.push(itemForCart)
18
19 window.localStorage.setItem('lineItems', JSON.stringify(arr))
20 }
21
22 return ()
23}

You can see that we are using that product prop returned from the getServerSideProps call. Then we create the addToCart function that checks the local storage for any existing cart items and it will push this current product to the cart when a customer clicks the button we'll add shortly. This is how we can preserve the cart items as customers navigate around the site.

Now we're ready to add the customer-facing functionality. Update the existing return statement with the following code:

1return (
2 <>
3 <div style={{ display: 'flex', justifyContent: 'space-evenly' }}>
4 <Image src={product.image} alt={product.description} height={250} width={250} />
5 <div style={{ width: '30%' }}>
6 <h2>{product.name}</h2>
7 <p>{product.category}</p>
8 <p>$ {product.price}</p>
9 <button onClick={addToCart}>Add to cart</button>
10 </div>
11 </div>
12 <Link passHref href={`/products`}>
13 <Back>Back to Products</Back>
14 </Link>
15 </>
16)

This will display all of the product info we discussed earlier along with a button to add the product to the cart and a link back to the products page. If you aren't running the app, start it back up with yarn dev, click on one of the products from the page that has all of the products, and you should see something similar to this.

You'll also see a URL similar to this: http://localhost:3000/product/62b6e542-33dd-4e88-98a0-c781c970e317 It has the product id we're using in that data request.

Go ahead and add the current product to the cart because we're about to make the page for it and having some data there will help.

Making the cart page

This is the last page for this app so we're almost there! Remember that we have the button to go to the cart on the products page. You could definitely add this button to the individual product pages if you like.

For now, let's work on this cart page. Add a new file to the pages directory called cart.tsx and add the following:

1import Image from 'next/image'
2import Link from 'next/link'
3import styled from 'styled-components'
4
5const Back = styled.div`
6 border: 1px solid #c4c4c4;
7 background-color: #abacab;
8 width: 150px;
9`
10
11const CartItem = styled.div`
12 border: 1px solid #c4c4c4;
13 display: flex;
14 height: 100px;
15 justify-content: space-between;
16 margin: 12px;
17 padding: 24px;
18`
19
20const Container = styled.div`
21 margin: 24px;
22 padding: 12px;
23`
24
25interface CartItem {
26 name: string
27 price: number
28 imageUrl: string
29}
30
31function Cart() {
32 return ()
33}
34
35export default Cart

This brings in all the imports we need, some styled components to bring a bit of organization to the list of items in the cart, and the type definition for the data we'll be handling. Since the cart items are stored in the browser's local storage, we don't need to request anything from the database on this page.

All that's left is getting the items from local storage and using our styled components to display them to the customers. So update the component with this code to get those items and handle the render state accordingly:

1function Cart() {
2 let cartItems
3
4 if (typeof window !== "undefined") {
5 cartItems = window.localStorage.getItem('lineItems')
6 }
7
8 if (!cartItems) {
9 return <div>No items in the cart</div>
10 }
11
12 const parsedCartItems = JSON.parse(cartItems)
13
14 return()
15}

This code will check the local storage for any cart items and if there aren't any, we'll render a message to the customer that the cart is empty. Otherwise, we'll parse the cart items so we can use the data. Now let's update that empty return statement to render all of those cart items:

1return (
2 <Container>
3 <h2>Tu Carrito</h2>
4 {
5 parsedCartItems.map((item: CartItem) => (
6 <CartItem key={item.name}>
7 <div>Name: {item.name}</div>
8 <div>Price: {item.price}</div>
9 <Image src={item.imageUrl} alt={item.name} height={100} width={100} />
10 </CartItem>
11 ))
12 }
13 <Link passHref href={`/products`}>
14 <Back>Back to Products</Back>
15 </Link>
16 </Container>
17)

We have a page header so that the customer knows they're on the cart page. Then we take all of the cart items and map the data to our CartItem styled component. This will create a styled list view of all the items. After we show all of the items, then we have a button to go back to the products page. This is a good place to add a button to some check-out functionality and that will likely require some third-party service.

At this point, run the app again and add a few items to the cart from their respective product pages. Then navigate to the /cart route and you'll see something like this.

Now the app has common pages that customers will go to in any online store! This can be cleaned up and turned into a full-fledged online store app that renders images quickly.

Finished code

If you want to check out the full repo, you can find the complete code in here.

Or you can check out the non-database code in this Code Sandbox.

Conclusion

Making a fast-loading online store is essential to getting customers to stay on your site. Waiting on images or pages to load will deter people from sticking around and it can cause your existing customers to look for other options. Using server-side rendering can help improve your customer experience and make your app more secure, so make sure it's a consideration before you build!

Milecia

Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.