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 ProductModel
lib\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
