Building An E-Commerce Store With NextJS

Building An E-Commerce Store With NextJS

In this tutorial, you’ll learn how to build an e-commerce store where customers can purchase products and make payments via Stripe. After a successful payment, an email notification is sent to the customer, and an in-app notification to the Admin user. The Admin user can also create and delete products within the application.

To build this application, we’ll use the following tools:

Appwrite – for authenticating users, as well as saving and retrieving product details.

Next.js – for creating the application’s user interface and backend.

Novu  – for sending email and in-app notifications.

React Email – for creating email templates.

Stripe – for integrating a payment checkout to the application.

Building the application interface with Next.js

The application pages are divided into two parts based on the roles assigned to the users. Customers can access the Home page and sign in to the application before making payments. Admin users can access all pages, including a sign-in page and a dashboard page where they can add and remove products.

Now, let’s build the application.

Create a new Next.js Typescript project by running the code snippet below:

npx create-next-app novu-store

Next, install React Icons and the Headless UI package. React Icons allows us to use various icons within the application, while Headless UI provides easy-to-use modern UI components.

npm install react-icons @headlessui/react

Copy this code snippet from the GitHub repository into the app/page.tsx file. It renders a list of products on the screen and allows users to select items in a cart, similar to the image below.

Create a login route that enables users to sign using their GitHub account. Copy the code snippet below into the app/login/page.tsx file.

//👉🏻 create a login folder containing a page.tsx file
export default function Home() {
const handleGoogleSignIn = async () => {};

return (
<main className=‘w-full min-h-screen flex flex-col items-center justify-center’>
<h2 className=‘font-semibold text-3xl mb-2’>Customer Sign in</h2>
<p className=‘mb-4 text-sm text-red-500’>
You need to sign in before you can make a purchase
</p>
<button
className=‘p-4 border-[2px] border-gray-500 rounded-md hover:bg-black hover:text-white w-2/3’
onClick={() => handleGoogleSignIn()}
>
Sign in with GitHub
</button>
</main>
);
}

When users click the Sign in button, it redirects them to the GitHub authentication page and prompts them to sign in to the application. You’ll learn how to do this with Appwrite shortly.

Next, let’s create the admin pages. Add an admin folder containing a login and dashboard route within the app folder.

cd app
mkdir admin && cd admin
mkdir dashboard login

Add a page.tsx file within the dashboard and login folders, and copy the code snippet below into the login/page.tsx file.

use client;
import Link from next/link;
import { useState } from react;

export default function Login() {
const [email, setEmail] = useState<string>(“”);
const [password, setPassword] = useState<string>(“”);

const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
console.log({ email, password });
};

return (
<main className=‘w-full min-h-screen flex flex-col items-center justify-center’>
<h2 className=‘font-semibold text-3xl mb-4’> Admin Sign in</h2>
<form className=‘w-2/3’ onSubmit={handleLogin}>
<label htmlFor=’email’ className=‘block’>
Email
</label>
<input
type=’email’
id=’email’
className=‘w-full px-4 py-3 border border-gray-400 rounded-sm mb-4’
required
value={email}
placeholder=‘admin@admin.com’
onChange={(e) => setEmail(e.target.value)}
/>

<label htmlFor=‘password’ className=‘block’>
Password
</label>
<input
type=‘password’
id=‘password’
className=‘w-full px-4 py-3 border border-gray-400 rounded-sm mb-4’
required
value={password}
placeholder=‘admin123’
onChange={(e) => setPassword(e.target.value)}
/>
<button className=‘p-4 text-lg mb-3 bg-blue-600 text-white w-full rounded-md’>
Sign in
</button>
<p className=‘text-sm text-center’>
Not an Admin?{ }
<Link href=‘/login’ className=‘text-blue-500’>
Sign in as a Customer
</Link>
</p>
</form>
</main>
);
}

The code snippet above renders a form that accepts the Admin’s email and password, validates the credentials, and then logs the user into the application.

The Admin dashboard page renders the available products and allows the Admin user to add and delete products from the application. Copy this code snippet into the dashboard/page.tsx file to create the user interface.

Congratulations! You’ve built the application interface. In the upcoming sections, you’ll learn how to connect the application to an Appwrite backend and send data between the client and the server.

How to add Appwrite to a Next.js application

Appwrite is an open-source backend service that enables you to create secure and scalable software applications. It offers features such as multiple authentication methods, a secure database, file storage, cloud messaging, and more, which are essential for building full-stack applications.

In this section, you’ll learn how to set up an Appwrite project, including features such as authentication, database, and file storage.

First, visit Appwrite Cloud, and create an account and organization for your projects.

Next, create a new project and select your preferred region for hosting the project.

Select Web as the platform SDK for the application.

Follow the steps displayed on the screen. Since you’re currently building in development mode, you can use the wildcard (*) as your hostname and change it to your domain name after deploying the application.

Install the Appwrite client SDK within your Next.js project.

npm install appwrite

Finally, create an appwrite.ts file within your Next.js app folder and copy the code snippet below into the file to initialize Appwrite.

import { Client, Account, Databases, Storage } from appwrite;

const client = new Client();

client
.setEndpoint(https://cloud.appwrite.io/v1)
.setProject(<YOUR_PROJECT_ID>);

export const account = new Account(client);

export const db = new Databases(client);

export const storage = new Storage(client);

Setting up GitHub Authentication with Appwrite

Here, you’ll learn how to set up GitHub and Email/Password authentication with Appwrite. Email/Password authentication is already configured by default, so let’s focus on setting up GitHub authentication.

Before we proceed, you need to create a GitHub OAuth application using your GitHub account. Appwrite will require the client ID and secrets to set up GitHub authentication.

Enable Appwrite’s GitHub authentication method by selecting Auth from the sidebar menu and navigating to the Settings tab.

Copy your GitHub client ID and secret into the Appwrite’s GitHub OAuth settings.

Finally, ensure you copy the URI generated by Appwrite into your GitHub app settings.

Setting up Appwrite Database

Select Databases from the sidebar menu and create a new database. You can name it novu store.

Next, create a products collection. It will contain the lists of products within the application.

Add name, price, and image attributes to the collection.

Under the Settings tab, update the permissions to allow every user to perform CRUD operations. However, you can change this after deploying the application to ensure that only authenticated users can perform various actions.

Finally, copy your project, database, and collection IDs into an .env.local file. This keeps your credentials safe and allows you to reference each value from its environment variables.

NEXT_PUBLIC_PROJECT_ID=<YOUR_PROJECT_ID>
NEXT_PUBLIC_DB_ID=<YOUR_DATABASE_ID>
NEXT_PUBLIC_PRODUCTS_COLLECTION_ID=<YOUR_DB_COLLECTION_ID>

Setting up Appwrite Storage

Select Storage from the sidebar menu and create a new bucket that will hold all the product images.

Under the Settings tab, update the Permissions to allow any user for now.

Set the acceptable file formats. Since we are uploading images, you can select the .jpg and .png file formats.

Finally, copy your bucket ID into .env.local file.

NEXT_PUBLIC_BUCKET_ID=<YOUR_BUCKET_ID>

Congratulations! You’ve successfully configured Appwrite. We can now start interacting with its various features.

How to perform CRUD operations with Appwrite

In this section, you’ll learn how to create, retrieve, and delete products from Appwrite. Users need to be able to view existing products before making a purchase, while Admin users should have the permission to add and delete products from the application.

First, create a utils.ts file within the Next.js app folder. This file will contain all Appwrite database interactions, which you can then import into the necessary pages.

cd app
touch utils.ts

Saving products to Appwrite

Recall that the products collection has three attributes: name, image, and price. Therefore, when adding products to the database, you need to first upload the product’s image, retrieve its URL and ID from the response, and then upload the URL as the product’s image attribute, using the image’s storage ID for the product data.

Here is the code snippet that explains this:

import { db, storage } from @/app/appwrite;
import { ID } from appwrite;

export const createProduct = async (
productTitle: string,
productPrice: number,
productImage: any
) => {
try {
//👇🏻 upload the image
const response = await storage.createFile(
process.env.NEXT_PUBLIC_BUCKET_ID!,
ID.unique(),
productImage
);
//👇🏻 get the image’s URL
const file_url = `https://cloud.appwrite.io/v1/storage/buckets/${process.env.NEXT_PUBLIC_BUCKET_ID}/files/${response.$id}/view?project=${process.env.NEXT_PUBLIC_PROJECT_ID}&mode=admin`;

//👇🏻 add the product to the database
await db.createDocument(
process.env.NEXT_PUBLIC_DB_ID!,
process.env.NEXT_PUBLIC_PRODUCTS_COLLECTION_ID!,
response.$id, //👉🏻 use the image’s ID
{
name: productTitle,
price: productPrice,
image: file_url,
}
);
alert(Product created successfully);
} catch (err) {
console.error(err);
}
};

The code snippet above uploads the image to Appwrite’s cloud storage and retrieves the exact image URL using the bucket ID, image ID, and project ID. Once the image is successfully uploaded, its ID is used in the product’s data to enable easy retrieval and reference.

Retrieving products from Appwrite

To fetch the products from Appwrite, you can execute the function below within the React useEffect hook when the page loads.

export const fetchProducts = async () => {
try {
const products = await db.listDocuments(
process.env.NEXT_PUBLIC_DB_ID!,
process.env.NEXT_PUBLIC_PRODUCTS_COLLECTION_ID!
);
if (products.documents) {
return products.documents;
}
} catch (err) {
console.error(err);
}
};

The fetchProducts function returns all the data within the products collection.

Deleting products from Appwrite

Admin users can also delete a product via its ID. The deleteProduct function accepts the product’s ID as a parameter and deletes the selected product from the database, including its image, since they use the same ID attribute.

export const deleteProduct = async (id: string) => {
try {
await db.deleteDocument(
process.env.NEXT_PUBLIC_DB_ID!,
process.env.NEXT_PUBLIC_PRODUCTS_COLLECTION_ID!,
id
);
await storage.deleteFile(process.env.NEXT_PUBLIC_BUCKET_ID!, id);

alert(Product deleted successfully);
} catch (err) {
console.error(err);
}
};

How to authenticate users with Appwrite

In the previous sections, we’ve configured the GitHub authentication method. Here, you’ll learn how to handle user sign-ins into the application.

To enable customers to sign into the application using their GitHub account, execute the function below when they click the Sign in button. The function redirects the user to GitHub, where they can authorize or grant permission to the application and then sign into the application:

import { account } from ../appwrite;
import { OAuthProvider } from appwrite;

const handleGoogleSignIn = async () => {
try {
account.createOAuth2Session(
OAuthProvider.Github,
http://localhost:3000,
http://localhost:3000/login
);
} catch (err) {
console.error(err);
}
};

Admin users can sign into the application using an email and password. Appwrite validates the credentials before granting access to the application’s dashboard.

import { account } from @/app/appwrite;

const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
try {
await account.createEmailPasswordSession(email, password);
alert(`Welcome back 🎉`);
router.push(/admin/dashboard);
} catch (err) {
console.error(err);
alert(Invalid credentials ❌);
}
};

Appwrite also allows you to fetch the current user’s data. For instance, if only authenticated users can make payments, you can do this by running the code snippet below. It retrieves the current user’s data or returns null if the user is not logged in.

import { account } from @/app/appwrite;

useEffect(() => {
const checkAuthStatus = async () => {
try {
const request = await account.get();
setUser(request);
} catch (err) {
console.log(err);
}
};
checkAuthStatus();
}, []);

How to add Stripe payment checkout to Next.js

In this section, you’ll learn how to implement a Stripe payment checkout in the application. Stripe is a popular online payment processing platform that enables you to create products and integrate both one-time and recurring payment methods into your application.

First, you need to create a Stripe account. You can use a test mode account for this tutorial.

Click on Developers from the top menu and copy your secret key from the API keys menu.

Paste your Stripe secret key into the .env.local file.

STRIPE_SECRET_KEY=<your_secret_key>

Install the Stripe Node.js SDK.

npm install stripe

Next, create an api folder within the Next.js app folder. The api folder will contain all the API routes and endpoints for the application.

cd app
mkdir api

Create a checkout endpoint by adding a checkout folder within the api folder.

cd api
mkdir checkout && cd checkout
touch route.ts

Copy the code snippet below into the route.ts file.

import { NextRequest, NextResponse } from next/server;
import Stripe from stripe;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: NextRequest) {
//👇🏻 accepts the customer’s cart
const cart = await req.json();

try {
//👇🏻 creates a checkout session
const session = await stripe.checkout.sessions.create({
payment_method_types: [card],
line_items: cart.map((product: Product) => ({
price_data: {
currency: usd,
product_data: {
name: product.name,
},
unit_amount: product.price * 100,
},
quantity: 1,
})),
mode: payment,
cancel_url: `http://localhost:3000/?canceled=true`,
success_url: `http://localhost:3000?success=true&session_id={CHECKOUT_SESSION_ID}`,
});
//👇🏻 return the session URL
return NextResponse.json({ session: session.url }, { status: 200 });
} catch (err) {
return NextResponse.json({ err }, { status: 500 });
}
}

The code snippet above creates a checkout endpoint that accepts POST requests. It creates a checkout session for the customer and returns the session URL.

The cancel_url and success_url determine where to redirect the user after completing or canceling a payment.

Finally, you can send a customer’s cart to the /checkout endpoint when a user decides to make payment for products by running the code snippet below:

const processPayment = async (cart: Product[]) => {
try {
if (user !== null) {
//👇🏻 saves cart to local storage
localStorage.setItem(cart, JSON.stringify(cart));
//👇🏻 sends cart to /checkout route
const request = await fetch(/api/checkout, {
method: POST,
body: JSON.stringify(cart),
headers: { Content-Type: application/json },
});
//👇🏻 retrieves the session URL
const { session } = await request.json();
//👇🏻 redirects the user to the checkout page
window.location.assign(session);
} else {
//👇🏻 redirects unauthenticated users
router.push(/login);
}
} catch (err) {
console.error(err);
}
};

The code snippet above saves the cart to the browser’s local storage and sends it to the API endpoint, then retrieves the response (session URL) from the backend server and redirects the user to the Stripe checkout page.

Sending in-app and email notifications with Novu

Novu is the first notification infrastructure that provides a unified API for sending notifications through multiple channels, including In-App, Push, Email, SMS, and Chat.

In this section, you’ll learn how to add Novu to your application to enable you to send email and in-app messages.

First, install the required Novu packages:

npm install @novu/node @novu/echo @novu/notification-center

When users make a purchase, they will receive a payment confirmation email, and the admin user also receives an in-app notification.

To do this, you need to create an account on Novu and set up a primary email provider. We’ll use Resend for this tutorial.

After creating an account on Novu, create a Resend account, and select API Keys from the sidebar menu on your dashboard to create one.

Next, return to your Novu dashboard, select Integrations Store from the sidebar menu, and add Resend as an email provider. You’ll need to paste your Resend API key and email address into the required fields.

Select Settings from the sidebar menu and copy your Novu APIkey and App ID into a .env.local file as shown below. Also, copy your subscriber ID into its field – you can get this from the Subscribers section.

NOVU_API_KEY=<YOUR_API_FOR_NEXT_SERVER>
NEXT_PUBLIC_NOVU_API_KEY=<YOUR_API_FOR_NEXT_CLIENT>

NEXT_PUBLIC_NOVU_APP_ID=<YOUR_API_ID>

NOVU_SUBSCRIBER_ID=<YOUR_API_FOR_NEXT_SERVER>
NEXT_PUBLIC_NOVU_SUBSCRIBER_ID=<YOUR_API_FOR_CLIENT>

Finally, add the Novu notification bell to the Admin dashboard to enable admin users to receive notifications within the application.

import {
NovuProvider,
PopoverNotificationCenter,
NotificationBell,
} from @novu/notification-center;

export default function AdminNav() {
return (
<NovuProvider
subscriberId={process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID!}
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
>
<PopoverNotificationCenter colorScheme=‘light’>
{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
</PopoverNotificationCenter>
</NovuProvider>
);
}

How to create notification workflows with Novu Echo

Novu offers a code-first workflow engine that enables you to create notification workflows within your codebase. It allows you to integrate email, SMS, and chat template and content generators, such as React Email and MJML, into Novu to create advanced and powerful notifications.

In this section, you’ll learn how to create notification workflows within your application, use email notification templates with Novu, and send in-app and email notifications with Novu.

Install React Email by running the following command:

npm install react-email @react-email/components -E

Include the following script in your package.json file. The –dir flag gives React Email access to the email templates located within the project. In this case, the email templates are located in the src/emails folder.

{
“scripts”: {
“email”: “email dev –dir src/emails”
}
}

Next, create an emails folder containing an email.tsx within the Next.js app folder and copy the code snippet below into the file:

import {
Body,
Column,
Container,
Head,
Heading,
Hr,
Html,
Link,
Preview,
Section,
Text,
Row,
render,
} from @react-email/components;
import * as React from react;

const EmailTemplate = ({
message,
subject,
name,
}: {
message: string;
subject: string;
name: string;
}) => (
<Html>
<Head />
<Preview>{subject}</Preview>
<Body style={main}>
<Container style={container}>
<Section style={header}>
<Row>
<Column style={headerContent}>
<Heading style={headerContentTitle}>{subject}</Heading>
</Column>
</Row>
</Section>

<Section style={content}>
<Text style={paragraph}>Hey {name},</Text>
<Text style={paragraph}>{message}</Text>
</Section>
</Container>

<Section style={footer}>
<Text style={footerText}>
You&apos;re receiving this email because your subscribed to Newsletter
App
</Text>

<Hr style={footerDivider} />
<Text style={footerAddress}>
<strong>Novu Store</strong>, &copy;{ }
<Link href=‘https://novu.co’>Novu</Link>
</Text>
</Section>
</Body>
</Html>
);

export function renderEmail(inputs: {
message: string;
subject: string;
name: string;
}) {
return render(<EmailTemplate {inputs} />);
}

const main = {
backgroundColor: #f3f3f5,
fontFamily: HelveticaNeue,Helvetica,Arial,sans-serif,
};

const headerContent = { padding: 20px 30px 15px };

const headerContentTitle = {
color: #fff,
fontSize: 27px,
fontWeight: bold,
lineHeight: 27px,
};

const paragraph = {
fontSize: 15px,
lineHeight: 21px,
color: #3c3f44,
};

const divider = {
margin: 30px 0,
};

const container = {
width: 680px,
maxWidth: 100%,
margin: 0 auto,
backgroundColor: #ffffff,
};

const footer = {
width: 680px,
maxWidth: 100%,
margin: 32px auto 0 auto,
padding: 0 30px,
};

const content = {
padding: 30px 30px 40px 30px,
};

const header = {
borderRadius: 5px 5px 0 0,
display: flex,
flexDireciont: column,
backgroundColor: #2b2d6e,
};

const footerDivider = {
divider,
borderColor: #d6d8db,
};

const footerText = {
fontSize: 12px,
lineHeight: 15px,
color: #9199a1,
margin: 0,
};

const footerLink = {
display: inline-block,
color: #9199a1,
textDecoration: underline,
fontSize: 12px,
marginRight: 10px,
marginBottom: 0,
marginTop: 8px,
};

const footerAddress = {
margin: 4px 0,
fontSize: 12px,
lineHeight: 15px,
color: #9199a1,
};

The code snippet above creates an customizable email template using React Email. You can find more easy-to-edit inspirations or templates. The component also accepts a message, subject, and name as props, and fills them into the elements.

Finally, you can run npm run email in your terminal to preview the template.

Next, let’s integrate the email template to Novu Echo. First, close the React Email server, and run the code snippet below. It opens the Novu Dev Studio in your browser.

npx novu-labs@latest echo

Create an echo folder containing a client.ts file within the Next.js app folder and copy this code snippet into the file.

import { Echo } from @novu/echo;
import { renderEmail } from @/app/emails/email;

interface EchoProps {
step: any;
payload: {
subject: string;
message: string;
name: string;
totalAmount: string;
};
}
export const echo = new Echo({
apiKey: process.env.NEXT_PUBLIC_NOVU_API_KEY!,
devModeBypassAuthentication: process.env.NODE_ENV === development,
});

echo.workflow(
novu-store,
async ({ step, payload }: EchoProps) => {
//👇🏻 in-app notification step
await step.inApp(notify-admin, async () => {
return {
body: `${payload.name} just made a new purchase of ${payload.totalAmount} 🎉`,
};
});
//👇🏻 email notification step
await step.email(
email-customer,
async () => {
return {
subject: `${payload ? payload?.subject : No Subject}`,
body: renderEmail(payload),
};
},
{
inputSchema: {
type: object,
properties: {},
},
}
);
},
{
payloadSchema: {
type: object,
properties: {
message: {
type: string,
default: Congratulations! Your purchase was successful! 🎉,
},
subject: { type: string, default: Message from Novu Store },
name: { type: string, default: User },
totalAmount: { type: string, default: 0 },
},
required: [message, subject, name, totalAmount],
additionalProperties: false,
},
}
);

The code snippet defines a Novu notification workflow named novu-store, which accepts a payload containing the email subject, message, the customer’s name and the total amount.

The workflow has two steps: in-app and email notification. The in-app notification sends a message to the Admin using the notification bell and the email sends a message to the customer’s email.

Next, you need to create an API route for Novu Echo. Within the api folder, create an email folder containing a route.ts file and copy the provided code snippet below into the file.

import { serve } from @novu/echo/next;
import { echo } from @/app/echo/client;

export const { GET, POST, PUT } = serve({ client: echo });

Run npx novu-labs@latest echo in your terminal. It will automatically open the Novu Dev Studio where you can preview your workflow and Sync it with the Cloud.

The Sync to Cloud button triggers a pop-up that provides instructions on how to push your workflow to the Novu Cloud.

To proceed, run the following code snippet in your terminal. This will generate a unique URL representing a local tunnel between your development environment and the cloud environment.

npx localtunnel –port 3000

Copy the generated link along with your Echo API endpoint into the Echo Endpoint field, click the Create Diff button, and deploy the changes.

https://<LOCAL_TUNNEL_URL>/<ECHO_API_ENDPOINT (/api/email)>

Congratulations! You’ve just created a Novu workflow from your codebase.

Finally, let’s create the endpoint that sends the email and in-app notifications when a user makes a payment. Create an api/send route and copy the code snippet below into the file:

import { NextRequest, NextResponse } from next/server;
import { Novu } from @novu/node;

const novu = new Novu(process.env.NOVU_API_KEY!);

export async function POST(req: NextRequest) {
const { email, name, totalAmount } = await req.json();

const { data } = await novu.trigger(novu-store, {
to: {
subscriberId: process.env.NOVU_SUBSCRIBER_ID!,
email,
firstName: name,
},
payload: {
name,
totalAmount,
subject: `Purchase Notification from Novu Store`,
message: `Your purchase of ${totalAmount} was successful! 🎉`,
},
});
console.log(data.data);

return NextResponse.json(
{
message: Purchase Completed!,
data: { novu: data.data },
success: true,
},
{ status: 200 }
);
}

The endpoint accepts the customer’s email, name, and total amount paid, and triggers the Novu notification workflow to send the required notifications after a payment is successful.

Conclusion

So far, you’ve learned how to do the following:

Implement multiple authentication methods, store, and retrieve data and files from Appwrite.
Create email templates with React Email, and send in-app and email notifications with Novu.

If you are looking forward to sending notifications within your applications, Novu is your best choice. With Novu, you can add multiple notification channels to your applications, including chat, SMS, email, push, and in-app notifications.

The source code for this tutorial is available here:
https://github.com/novuhq/ecom-store-with-nextjs-appwrite-novu-and-stripe

Thank you for reading!