Add Products to Database Using NextJS Server Actions (Part 2) : Building an E-Commerce Web App from Scratch with Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB
Welcome back to the second part of our blog series, "Building an E-Commerce Web App from Scratch with NextJS, Tailwind CSS, DaisyUI, Prisma, and MongoDB." In this installment, we will take a deep dive into creating a fundamental feature of any successful online store – an "Add Product" form page.
As we progress on this exciting journey, we'll be leveraging Next.js's server actions to implement a seamless process for adding new products to our database. By the end of this post, you'll have a fully functional form that empowers you to effortlessly expand your product catalog, ensuring your E-Commerce Web App continues to grow and thrive.
We understand the importance of a user-friendly and intuitive interface, and that's exactly what we'll be striving for. By combining the power of Tailwind CSS and DaisyUI, we'll create a visually appealing form that not only looks great but also enhances the overall user experience.
So, let's get ready to equip your E-Commerce Web App with the ability to showcase your latest products with ease. Are you excited? We certainly are! Let's dive into Part 2 - "Add Product Form Page" and continue transforming your vision into a reality!
Tutorial
Please note that this is in continuation to The Foundational Setup (Part 1) of this series. I would recommend going through Part 1 to understand the setup of this NextJS application and the MongoDB database. Thankyou!
Once we’ve got all the necessary setup done for the database and the application. We will run the development server with the following command in the project terminal:
npm run dev
This will open the dev server at https://localhost:3000 and display the following standard page:
Creating the Add Product Page
To create a new page, we will use the inherent app router and create a folder called add-product within the app directory within which we will create a page.tsx file where we will write the following code:
export default function AddProduct() {
return (
<div>
<h1>
Add Product
</h1>
</div>
)
}
Then after saving this, we can view this page in our browser by going to localhost:3000/add-product which will simply display the heading of the page for now.
In the previous article, Part 1, we had setup the prettier.config.js file which seems to be giving some issues while formatting code because with the latest updates in Tailwind we can delete the prettier config file and simply use the pettier plugin as a dev dependency which will format code automatically. Otherwise, earlier we were getting errors of invalid configuration file when trying to auto format on save.
Add styling to Overall Layout
In order to add styling to the complete website, we will add some styles to the layout.tsx in the app directory in the following way:
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "MegaCart",
description:
"A simple e-commerce app built with NextJS, MongoDB, Prisma, and TailwindCSS",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<main className="p-4 max-w-7xl m-auto min-w-[300px]">{children}</main>
</body>
</html>
);
}
The above code is a NexttJS component named RootLayout
, which serves as the layout for the entire application. Let's break down what each part of the code does:
import "./globals.css";
: This line imports a CSS file named "globals.css". This file likely contains global CSS styles that will be applied to the entire application.import type { Metadata } from "next";
: This line imports theMetadata
type from the "next" package.Metadata
is used to define metadata for a Next.js page, including attributes liketitle
anddescription
.import { Inter } from "next/font/google";
: This line imports theInter
font from Google Fonts using thenext/font/google
package. TheInter
font is assigned to a variable namedinter
.const inter = Inter({ subsets: ["latin"] });
: This line initializes theinter
font by calling theInter
function and specifying that only the "latin" subset should be used.export const metadata: Metadata = { ... }
: This exports a constant namedmetadata
, which is of typeMetadata
. It contains metadata for the page, including the page title and description.export default function RootLayout({ children }: { children: React.ReactNode; }) { ... }
: This exports the default function componentRootLayout
, which takes a prop namedchildren
of typeReact.ReactNode
. It wraps the entire content in an HTML structure, setting the language to "en" and applying theinter
font class to the<body>
element. The main content is wrapped in a<main>
element with some Tailwind CSS classes for styling.
In summary, this code sets up a basic Next.js layout with global styles, a custom font (Inter), and metadata for the page title and description. It will be used as a template for rendering the content of different pages in the application.
Create a basic Add Product Form
Now, within the page.tsx within add-product folder that we created before, we will create a baisc form using TailwindCSS and DaisyUI input components (Tailwind Text Input Component — Tailwind CSS Components (daisyui.com)) in the following way:
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Add Product - MegaCart",
};
export default function AddProduct() {
return (
<div className="max-w-lg m-auto h-screen">
<h1 className="font-bold text-lg mb-3 ">Add Product</h1>
<form>
<input
required
name="name"
placeholder="Product Name"
className="mb-3 input input-bordered w-full"
/>
<textarea
required
name="description"
placeholder="Product Description"
className="textarea-bordered textarea mb-3 w-full"
/>
<input
required
name="imageUrl"
placeholder="Product Image URL"
className="mb-3 input input-bordered w-full"
/>
<input
required
name="price"
placeholder="Product Price"
type="number"
className="mb-3 input input-bordered w-full"
/>
<button className="btn btn-primary btn-block" type="submit">
Add Product
</button>
</form>
</div>
);
}
The above code is a Next.js component named AddProduct
, which represents a form to add a new product in an e-commerce application. Let's break down what each part of the code does:
import { Metadata } from "next";
: This line imports theMetadata
type from the "next" package. TheMetadata
type is used to define metadata for a Next.js page, including attributes liketitle
.export const metadata: Metadata = { ... }
: This exports a constant namedmetadata
, which is of typeMetadata
. It contains metadata for the page, specifically the title, which will be displayed in the browser tab.export default function AddProduct() { ... }
: This exports the default function componentAddProduct
. It represents the form to add a new product in the application.- The component's JSX code represents the form structure using HTML elements and applies Tailwind CSS classes for styling.
- The form has several input fields for the product's name, description, image URL, and price. Each input field is marked as
required
, meaning it must be filled before submitting the form. - The form also contains a
<button>
element with the class "btn btn-primary btn-block" that serves as a submit button to add the product.
In summary, this code sets up a Next.js component that displays a form to add a new product. It includes basic input fields for product information and a submit button. The page title is set to "Add Product - MegaCart" based on the metadata configuration.
Using Server Actions to Add Products to Database
Earlier, we had to create separate API routes for data fetching and posting in order to add products on the server side. We could also use ExpressJS to create a separate server for this. But with NextJS server actions (Data Fetching: Server Actions | Next.js (nextjs.org)) we can do so within the same server side component that we’ve just created.
First, in order to allow server actions feature to run in the app, you’ll need to modify the next.config.ts file in the following way:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [{ hostname: "images.unsplash.com" }],
},
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;
Note that we’ve also added unplash url in the config because we will be using the the image urls from the unsplash website for product images in the form.
Before moving ahead, make sure you’ve generated the prisma client earlier with the command npx prisma generate
otherwise the prisma method won’t show the available model schema.
Remember that prisma mehtods can only run on server components and if trying to run them from client side, prisma won’t allow it and throw an error.
So now, we will modify and add the following code to the page.tsx in add-product folder:
import { Metadata } from "next";
import { prisma } from "@/app/lib/db/prisma";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Add Product - MegaCart",
};
async function addProduct(formData: FormData) {
"use server"; // tells NextJS this function should run on the server
// get the form data from the request body and add it to the database
const name = formData.get("name")?.toString();
const description = formData.get("description")?.toString();
const imageUrl = formData.get("imageUrl")?.toString();
const price = Number(formData.get("price") || 0);
// validate the form data before adding it to the database
if (!name || !description || !imageUrl || !price) {
throw Error("Missing required fields");
}
await prisma.product.create({
data: {
name,
description,
imageUrl,
price,
},
});
redirect("/");
}
export default function AddProduct() {
return (
<div className="max-w-lg m-auto h-screen">
<h1 className="font-bold text-lg mb-3 ">Add Product</h1>
<form action={addProduct}>
<input
required
name="name"
placeholder="Product Name"
className="mb-3 input input-bordered w-full"
/>
<textarea
required
name="description"
placeholder="Product Description"
className="textarea-bordered textarea mb-3 w-full"
/>
<input
required
name="imageUrl"
placeholder="Product Image URL"
className="mb-3 input input-bordered w-full"
/>
<input
required
name="price"
placeholder="Product Price"
type="number"
className="mb-3 input input-bordered w-full"
/>
<button className="btn btn-primary btn-block" type="submit">
Add Product
</button>
</form>
</div>
);
}
Above code is a Next.js component that represents a form to add a new product in an e-commerce application. It also includes a server-side function to handle the form submission and database interaction. Let's break down what each part of the code does:
import { Metadata } from "next";
: This line imports theMetadata
type from the "next" package. TheMetadata
type is used to define metadata for a Next.js page, including attributes liketitle
.import { prisma } from "@/lib/db/prisma";
: This line imports theprisma
object, which is an instance of the Prisma client used to interact with the database.import { redirect } from "next/navigation";
: This line imports theredirect
function from the "next/navigation" module. Theredirect
function is used to redirect the user to another page after the form is submitted successfully.export const metadata: Metadata = { ... }
: This exports a constant namedmetadata
, which is of typeMetadata
. It contains metadata for the page, specifically the title, which will be displayed in the browser tab.async function addProduct(formData: FormData) { ... }
: This is an async function namedaddProduct
, which will handle the form submission on the server-side. It takesformData
as an argument, which is the data submitted through the form.- The function contains code to extract the product information (name, description, image URL, and price) from the
formData
. It then validates the data to ensure that all required fields are provided. - If the form data is valid, the function uses the Prisma client (
prisma
) to add the new product to the database by callingprisma.product.create({ ... })
. - After adding the product to the database, the function uses
redirect("/")
to redirect the user back to the homepage. export default function AddProduct() { ... }
: This exports the default function componentAddProduct
. It represents the form to add a new product in the application.- The component's JSX code represents the form structure using HTML elements and applies Tailwind CSS classes for styling.
- The form has several input fields for the product's name, description, image URL, and price. Each input field is marked as
required
, meaning it must be filled before submitting the form. - The form's
action
attribute is set to theaddProduct
function, which means that when the form is submitted, it will call theaddProduct
function on the server-side.
In summary, this code sets up a Next.js component that displays a form to add a new product. When the form is submitted, the addProduct
function will be called on the server-side to validate the data and add the new product to the database. If the submission is successful, the user will be redirected back to the homepage.
The above image shows how the add product form will look like.
Now, we’ll try to add a product by manually adding the following information in the respective fields and view our MongoDB collection to check if the server action is actually working like its supposed to.
{
"name": "Polaroid Instant Camera",
"description": "Capture and print your memories instantly with this classic Polaroid Instant Camera. It's easy to use and delivers charming, vintage-style photos.",
"image_url": "<https://images.unsplash.com/photo-1526170375885-4d8ecf77b99f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80>",
"price": 999
}
When we click on Add Product button, it adds the data to the MongoDB collection and redirects us to the home page.
As you can see in the image above, a new document is created within the products collection with the same data as we submitted via the form. Hurray! The form is working!
Now, we cannot see a loading indicator on the add product button but it is not possible to add the indicator directly within the server component with the server actions since loading will require a state which is a client side feature.
So, to implement this loading indicator, we will extract the button from the form, create the functionality for it in a separate client side component, and then hook it to the server action.
First, let’s create a FormSubmitButton.tsx component in a components folder within the src directory with the following code:
"use client"; // tells NextJS this function should run on the client
import React, { ComponentProps } from "react";
import { experimental_useFormStatus as useFormStatus } from "react-dom"; // this is a custom hook that we created to get the status of the form. It will return true if the form is pending and false otherwise.
// generally we use interface instead of type for the props but here we need to extend the props of the button so we use type which we cannot achieve with interface.
type FormSubmitButtonProps = {
children: React.ReactNode; // this will allow whatever we pass as children to be rendered inside the button with the same props as the button
className?: string; // this will allow us to pass a className prop to the button from outside the component
} & ComponentProps<"button">;
// ComponentProps is a utility type that extracts the props of a component. In this case, we are extracting the props of the button component. It only works with types and not interfaces.
export default function FormSubmitButton(
{
children,
className,
...props // this will allow us to pass any other props to the button from outside the component
}: FormSubmitButtonProps // we are destructuring the props and passing them to the button
) {
const { pending } = useFormStatus(); // this will give us the status of the form. If the form is pending, it will return true otherwise false.
return (
<button
{...props} // this will pass all the props to the button
type="submit"
disabled={pending}
className={`btn btn-primary ${className}`}
>
{children}
{pending && <span className="loading loading-dots" />}
</button>
);
}
The above code is a React function component named FormSubmitButton
, which represents a custom button used for form submission. Let's break down what each part of the code does:
use client"; // tells NextJS this function should run on the client
: This comment indicates that the function should run on the client-side when the component is rendered in the browser.import React, { ComponentProps } from "react";
: This line imports the necessary modules for the component, includingReact
andComponentProps
.type FormSubmitButtonProps = { ... } & ComponentProps<"button">;
: This defines a TypeScript type namedFormSubmitButtonProps
, which extends the props of a standard HTML button (ComponentProps<"button">
). The type includes the following properties:children
: It allows any React nodes to be passed as children to be rendered inside the button.className?: string
: It allows the component to accept aclassName
prop to add custom CSS classes to the button.- Other button-related props: The
ComponentProps<"button">
extracts all the props that are available to a standard HTML button.
export default function FormSubmitButton({ ... }: FormSubmitButtonProps) { ... }
: This exports the default function componentFormSubmitButton
. It takes the destructuredFormSubmitButtonProps
as its parameter.const { pending } = useFormStatus();
: This line uses theuseFormStatus
custom hook (imported from"react-dom"
) to get the status of the form. TheuseFormStatus
hook returns an object with a property namedpending
, which istrue
if the form is pending (e.g., during form submission) andfalse
otherwise.- The
return
statement renders a<button>
element with the following properties:...props
: This spreads all the props passed to theFormSubmitButton
component, allowing any additional button-specific props to be added from outside the component.type="submit"
: Sets the button type to "submit," indicating that it is a form submission button.disabled={pending}
: The button will be disabled if thepending
value istrue
, i.e., when the form is being submitted.className={
btn btn-primary ${className}}
: Combines the "btn" and "btn-primary" CSS classes with the customclassName
passed to the component.
- The button's content is defined by the
children
prop, which can contain any React nodes (e.g., text or other components). {pending && <span className="loading loading-dots" />}
: If thepending
value istrue
, it renders a loading indicator (aspan
element with CSS classes "loading loading-dots").
In summary, this code defines a custom FormSubmitButton
component that renders a button with form submission functionality. It handles form submission status and displays a loading indicator while the form is being submitted. The component is flexible and accepts additional button-specific props and custom CSS classes.
For the different loading spinners, checkout, Tailwind Loading Component — Tailwind CSS Components (daisyui.com)
Now we will import this into the page.tsx under the add-product folder and replace the earlier button with the new one in the following way:
import { Metadata } from "next";
import { prisma } from "@/app/lib/db/prisma";
import { redirect } from "next/navigation";
import FormSubmitButton from "@/components/FormSubmitButton";
export const metadata: Metadata = {
title: "Add Product - MegaCart",
};
async function addProduct(formData: FormData) {
"use server"; // tells NextJS this function should run on the server
// get the form data from the request body and add it to the database
const name = formData.get("name")?.toString();
const description = formData.get("description")?.toString();
const imageUrl = formData.get("imageUrl")?.toString();
const price = Number(formData.get("price") || 0);
// validate the form data before adding it to the database
if (!name || !description || !imageUrl || !price) {
throw Error("Missing required fields");
}
await prisma.product.create({
data: {
name,
description,
imageUrl,
price,
},
});
redirect("/");
}
export default function AddProduct() {
return (
<div className="max-w-lg m-auto h-screen ">
<h1 className="font-bold text-lg mb-3 ">Add Product</h1>
<form action={addProduct}>
<input
required
name="name"
placeholder="Product Name"
className="mb-3 input input-bordered w-full"
/>
<textarea
required
name="description"
placeholder="Product Description"
className="textarea-bordered textarea mb-3 w-full"
/>
<input
required
name="imageUrl"
placeholder="Product Image URL"
className="mb-3 input input-bordered w-full"
/>
<input
required
name="price"
placeholder="Product Price"
type="number"
className="mb-3 input input-bordered w-full"
/>
<FormSubmitButton className="btn-block">Add Product</FormSubmitButton>
</form>
</div>
);
}
The above code is a Next.js component named AddProduct
that represents a form to add a new product in an e-commerce application. It utilizes server-side functionality to add the product to the database upon form submission. Let's break down the code:
import { Metadata } from "next";
: This line imports theMetadata
type from the "next" package. TheMetadata
type is used to define metadata for a Next.js page, including attributes liketitle
.import { prisma } from "@/app/lib/db/prisma";
: This line imports theprisma
object, which is an instance of the Prisma client used to interact with the database. Theprisma
object likely contains methods for database operations related to the "Product" model.import { redirect } from "next/navigation";
: This line imports theredirect
function from the "next/navigation" module. Theredirect
function is used to redirect the user to another page after the form is submitted successfully.import FormSubmitButton from "@/components/FormSubmitButton";
: This line imports theFormSubmitButton
component from the "@/components" directory. TheFormSubmitButton
is a custom component used as the submit button for the form.export const metadata: Metadata = { ... }
: This exports a constant namedmetadata
, which is of typeMetadata
. It contains metadata for the page, specifically the title, which will be displayed in the browser tab.async function addProduct(formData: FormData) { ... }
: This is an async function namedaddProduct
, which handles the form submission on the server-side. It takesformData
as an argument, which is the data submitted through the form.- The function extracts the product information (name, description, image URL, and price) from the
formData
. - It then validates the form data to ensure that all required fields are provided. If any of the required fields are missing, the function throws an
Error
with the message "Missing required fields". - If the form data is valid, the function uses the Prisma client (
prisma
) to add the new product to the database by callingprisma.product.create({ ... })
. - After adding the product to the database, the function uses
redirect("/")
to redirect the user back to the homepage. - The
export default function AddProduct() { ... }
block represents the main component that renders the form:
- The component renders a
<div>
element containing the form elements. - The form has various input fields for the product's name, description, image URL, and price, each marked as
required
. - The form uses the
FormSubmitButton
component as its submit button. - The
FormSubmitButton
component is used with the propclassName="btn-block"
to style the button as a block-level element.
In summary, this code sets up a Next.js component that displays a form to add a new product. When the form is submitted, the addProduct
function is called on the server-side to validate the data and add the new product to the database. The page title is set using metadata, and a custom FormSubmitButton
component is used for the form submission.
Now, let’s try the form with the below product data:
{
"name": "Vintage 35mm Film Camera",
"description": "Rediscover the golden age of photography with this elegant Vintage 35mm Film Camera. Crafted with precision and alluring details, it offers a unique shooting experience that digital cameras can't replicate. Embrace manual control, capture stunning images with its high-quality glass lens, and create timeless memories on film.",
"image_url": "<https://images.unsplash.com/photo-1511184059754-e4b5bbbcef75?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=580&q=80>",
"price": 249.99
}
If this was successful, you’ll see that when you click on the add product button, it becomes disabled for a moment and displays a loading indicator within the button before redirecting to the home page.
Adding a default error page
Lastly, we’ll just add a default error page to display when something goes wrong with the server requests. In NextJS, the standard way of doing this is by creating an error.tsx file in the app directory with the following basic code for the moment (we can style it in our own way later) :
"use client"; // tells NextJS this function should run on the client
export default function ErrorPage() {
return (
<div>Something went wrong, please refresh the page or try again later.</div>
);
}
The above code is a Next.js function component named ErrorPage
, which represents an error page that will be displayed on the client-side when something goes wrong. Let's break down what each part of the code does:
use client"; // tells NextJS this function should run on the client
: This comment indicates that the function should run on the client-side when the component is rendered in the browser. It means that this component is intended for client-side rendering and won't be executed on the server-side during server rendering.export default function ErrorPage() { ... }
: This exports the default function componentErrorPage
.- The component's JSX code represents the content of the error page, which is a simple
<div>
element displaying the message "Something went wrong, please refresh the page or try again later."
In summary, this code sets up a Next.js component named ErrorPage
that represents an error page to be displayed on the client-side when something goes wrong. It is a simple component with a single <div>
element containing an error message.
And with this, we’re done with the add product page where we have added two products to the database. To populate the database, we’ll further 2 more items before moving on to displaying them on the home page. You can use the below product details or create them on your own.
{
"name": "Canon EOS Rebel T7i DSLR Camera",
"description": "Unleash your creativity with the Canon EOS Rebel T7i DSLR Camera. Capture stunning photos and Full HD videos with its 24.2MP sensor and advanced autofocus system. The vari-angle touchscreen LCD, built-in Wi-Fi, NFC, and Bluetooth add convenience to your photography.",
"image_url": "<https://images.unsplash.com/photo-1502920917128-1aa500764cbd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=870&q=80>",
"price": 799.99
}
{
"name": "Complete Camera Kit with Bag",
"description": "Capture stunning images and videos with this Complete Camera Kit. It includes a high-quality DSLR camera, versatile lenses, and a premium camera bag for easy and secure transportation. The camera features a 24.2MP sensor, advanced autofocus, Wi-Fi, NFC, and a flip-out touchscreen for creative shooting. Perfect for both photography enthusiasts and content creators.",
"image_url": "<https://images.unsplash.com/photo-1571689936008-083b32a9dcca?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1042&q=80>",
"price": 1099.99
}
With this, we will have 4 products within the MongoDB collection as you can see in the dashboard:
Next up, we will display these products on our home page.
Conclusion to Part 2
Congratulations on successfully adding the "Add Product" form page to your E-Commerce Web App! In this second part of our series, we've empowered your online store with a crucial feature that allows you to effortlessly expand your product catalog.
By harnessing the power of Next.js's server actions, we've ensured that the process of adding new products to your database is seamless and efficient. The integration of Tailwind CSS and DaisyUI has not only made the form visually appealing but also contributed to an enhanced user experience, keeping your customers engaged and satisfied.
But our journey doesn't end here! In the next part of this series, we'll take a step further and unveil an exciting new feature: displaying the added products on the home page. Your E-Commerce Web App will come to life as customers can now explore and interact with your diverse range of products right from the main page.
We understand that building a successful E-Commerce platform is all about offering a seamless shopping experience, and that's exactly what we'll be focusing on in the upcoming post. By the end of it, you'll have a home page that not only showcases your products beautifully but also encourages users to make that perfect purchase.
As always, we'll guide you through each step, ensuring that you have a clear understanding of the process. Get ready to enhance your customers' shopping journey and elevate your online store to new heights.
Thank you for joining us on this incredible journey of building an E-Commerce Web App from scratch. We hope this series has been informative and enjoyable, inspiring you to take your project to new heights.
Stay tuned for the next installment, and remember, with Next.js, Tailwind CSS, DaisyUI, Prisma, and MongoDB in your toolkit, there's no limit to what you can achieve. Happy coding!