Install nextjs npx create-next-app@latest https://nextjs.org/docs/getting-started/installation
Install the following
npm i mongoose-zod
https://github.com/andreww2012/mongoose-zod
A library which allows to author mongoose ("a MongoDB object modeling tool") schemas using zod ("a TypeScript-first schema declaration and validation library").
npm install react-daisyui
https://www.npmjs.com/package/react-daisyui
daisyUI components built with React, Typescript and TailwindCSS
npm install react-hot-toast
https://www.npmjs.com/package/react-hot-toast
Add beautiful notifications to your React app with react-hot-toast.
edit tailwind.config.ts Add daisyui to plugins
edit tailwind.config.ts
//edit tailwind.config.ts import type { Config } from 'tailwindcss' const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', }, }, }, plugins: [require('daisyui')], //https://www.npmjs.com/package/react-daisyui } export default config.env
MONGODB_URI=mongodb://127.0.0.1/nextjs14app\page.tsx
//app\page.tsx import Image from 'next/image' //https://www.npmjs.com/package/react-hot-toast //https://www.npmjs.com/package/react-daisyui //https://github.com/andreww2012/mongoose-zod //https://www.npmjs.com/package/mongoose //npm i mongoose zod daisyui react-hot-toast import { Toaster } from 'react-hot-toast' import CreateForm from './create-form' import dbConnect from '@/lib/db-connect' import ProductModel, { Product } from '@/lib/product-model' import DeleteForm from './delete-form' export default async function Home() { await dbConnect() const products = (await ProductModel.find({}).sort({ _id: -1, })) as Product[] return ( <div className="mx-auto max-w-2xl lg:max-w-7xl"> <div className="flex justify-between items-center"> <h1 className="font-bold py-10 text-2xl">Next.js 14 Server Actions MongoDB - List all data, Create Product and Delete</h1> <Toaster /> <CreateForm /> </div> <div className="inline-block min-w-full align-middle"> <table className="min-w-full divide-y divide-gray-200 table-fixed dark:divide-gray-700"> <thead className="bg-gray-100 dark:bg-gray-700"> <tr> <th scope="col" className="p-4"> <div className="flex items-center"> <input id="checkbox-all" type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" /> <label htmlFor="checkbox-all" className="sr-only">checkbox</label> </div> </th> <th className="py-3 text-left">Image</th> <th className="py-3 text-left">Product Name</th> <th className="py-3 text-left">Price</th> <th className="py-3 text-left">Category</th> <th className="py-3 text-left">Actions</th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700"> {products.length === 0 ? ( <tr> <td colSpan={5}>No product found</td> </tr> ) : ( products.map((product: Product) => ( <tr key={product._id} className="hover:bg-gray-100 dark:hover:bg-gray-700"> <td className="p-4 w-4"> <div className="flex items-center"> <input id="checkbox-table-1" type="checkbox" className="w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" /> <label htmlFor="checkbox-table-1" className="sr-only">checkbox</label> </div> </td> <td> <Image src={product.image} alt={product.name} width={80} height={80} className="rounded-lg" /> </td> <td>{product.name}</td> <td>${product.price}</td> <td>{product.category}</td> <td> <DeleteForm _id={product._id.toString()} name={product.name} /> </td> </tr> )) )} </tbody> </table> </div> </div> ) }app\layout.tsx
//app\layout.tsx import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Next.js 14 Server Actions MongoDB - List all data, Create and Delete', description: 'Generated by create next app', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body className={inter.className}>{children}</body> </html> ) }app\create-form.tsx
// 'use client' import { useFormState, useFormStatus } from 'react-dom' import { createProduct } from '@/lib/actions' import { useEffect, useRef } from 'react' import toast from 'react-hot-toast' export default function CreateForm() { const [state, formAction] = useFormState(createProduct, { message: '', }) const { pending } = useFormStatus() const ref = useRef<HTMLFormElement>(null) useEffect(() => { if (state.message.indexOf('Created product') === 0) { ; (document.getElementById('my_modal') as any)!.close() ref.current?.reset() toast(state.message) } else if (state.message) { toast(state.message) } }, [state.message]) return ( <div> <button className="btn btn-primary" onClick={() => (document.getElementById('my_modal')! as any).showModal() } > Create Product </button> <dialog id="my_modal" className="modal"> <div className="modal-box"> <h2 className="tex-2xl font-bold pm-4">Create Product</h2> <form ref={ref} action={formAction}> <div className="form-control w-full max-w-xs py-4"> <label htmlFor="name">Name</label> <input type="text" id="name" name="name" className="input input-bordered w-full max-w-xs" required /> </div> <div className="form-control w-full max-w-xs py-4"> <label htmlFor="image">Image</label> <input type="text" id="image" name="image" className="input input-bordered w-full max-w-xs" required defaultValue="/images/1.jpg" /> </div> <div className="form-control w-full max-w-xs py-4"> <label htmlFor="price">Price</label> <input type="number" id="price" name="price" className="input input-bordered w-full max-w-xs" required defaultValue="1" /> </div> <div className="form-control w-full max-w-xs py-4"> <label htmlFor="name">Category</label> <input type="text" id="category" name="category" className="input input-bordered w-full max-w-xs" required /> </div> <button className="btn btn-primary mr-3" type="submit" disabled={pending} > Create </button> <button type="button" className="btn btn-ghost" onClick={() => (document.getElementById('my_modal') as any).close() } > Back </button> </form> </div> </dialog> </div> ) }app\delete-form.tsx
//app\delete-form.tsx 'use client' import { deleteProduct } from '@/lib/actions' import { useFormStatus } from 'react-dom' import toast from 'react-hot-toast' export default function DeleteForm({ _id, name, }: { _id: string name: string }) { const { pending } = useFormStatus() return ( <form action={async (formData) => { const res = await deleteProduct(formData) toast(res.message) }} > <input type="hidden" name="_id" value={_id} /> <input type="hidden" name="name" value={name} /> <button type="submit" disabled={pending} className="btn btn-ghost"> Delete </button> </form> ) }lib\db-connect.ts
//lib\db-connect.ts import mongoose from 'mongoose' export default async function dbConnect() { try { await mongoose.connect(process.env.MONGODB_URI!) console.log("Success Connection"); } catch (error) { throw new Error('Connection failed!') } }lib\product-model.ts
//lib\product-model.ts import mongoose from 'mongoose' export type Product = { _id: string name: string image: string price: number category: string } const productSchema = new mongoose.Schema( { name: { type: String, required: true, unique: true }, image: { type: String, required: true }, price: { type: Number, required: true }, category: { type: String, required: true }, }, { timestamps: true, } ) const ProductModel = mongoose.models.Product || mongoose.model('Product', productSchema) export default ProductModellib\actions.ts
//lib\actions.ts 'use server' import { revalidatePath } from 'next/cache' import ProductModel from './product-model' import dbConnect from './db-connect' import { z } from 'zod' export async function createProduct(prevState: any, formData: FormData) { const schema = z.object({ name: z.string().min(3), image: z.string().min(1), price: z.number().min(1), category: z.string().min(1), }) const parse = schema.safeParse({ name: formData.get('name'), image: formData.get('image'), price: Number(formData.get('price')), category: formData.get('category'), }) if (!parse.success) { console.log(parse.error) return { message: 'Form data is not valid' } } const data = parse.data try { await dbConnect() const product = new ProductModel(data) await product.save() revalidatePath('/') return { message: `Created product ${data.name}` } } catch (e) { return { message: 'Failed to create product' } } } export async function deleteProduct(formData: FormData) { const schema = z.object({ _id: z.string().min(1), name: z.string().min(1), }) const data = schema.parse({ _id: formData.get('_id'), name: formData.get('name'), }) try { await dbConnect() await ProductModel.findOneAndDelete({ _id: data._id }) revalidatePath('/') console.log({ message: `Deleted product ${data.name}` }) return { message: `Deleted product ${data.name}` } } catch (e) { return { message: 'Failed to delete product' } } }run C:\nextjs>npm run dev