Next.js
Install requirements
Zod
Zod is a TypeScript-first schema declaration and validation library.
https://www.npmjs.com/package/zod
Prisma is an open-source next-generation ORM. It consists of the following parts: Prisma Client: Auto-generated and type-safe query builder
https://www.prisma.io
Install prisma/client
npm install @prisma/client
https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/generating-prisma-client
Generate Prisma Client with the following command:
npx prisma generate
Install prisma
npm install prisma --save-dev
https://www.prisma.io/docs/getting-started/quickstart
set up Prisma with the init command of the Prisma CLI:
npx prisma init --datasource-provider sqlite
to npx prisma init --datasource-provider postgres
Model data in the Prisma schema
prisma/schema.prisma
model Upload {
id String @id @default(cuid())
title String
image String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Run a migration to create your database tables with Prisma Migrate
npx prisma migrate dev --name init
.env
DATABASE_URL="postgresql://postgres:admin@localhost:5432/postgresDB?schema=public"
https://www.prisma.io/docs/orm/prisma-client/queries/crud
https://nextjs.org/docs/app/building-your-application/routing/route-handlers
app\page.tsx
//app\page.tsx import Image from "next/image"; import Link from "next/link"; import { DeleteButton, EditButton } from "@/components/button"; import { getImages } from "@/lib/data"; export default async function Home() { const getimages = await getImages(); return ( <div className="max-w-screen-lg mx-auto py-14"> <h1 className="text-4xl font-bold">Nextjs 14 CRUD Create,Read,Update and Delete with upload and delete image Server-Side | Postgresql Prisma</h1> <div className="flex items-end justify-between m-12"> <h1 className="text-4xl font-bold">Images</h1> <Link href="/create" className="py-3 px-6 bg-green-700 hover:bg-green-800 text-white" > Upload New Image </Link> </div> <div className="grid md:grid-cols-3 gap-5 mt-10"> {getimages.map((item) => ( <div key={item.id} className="max-w-sm border border-gray-200 rounded-md shadow"> <div className="relative aspect-video"> <Image src={`http://localhost:3000/assets/${item.image}`} alt={item.title} fill priority sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" className="rounded-t-md object-cover" /> </div> <div className="p-5"> <h1 className="text-2xl font-bold text-gray-900 truncate"> {item.title} </h1> </div> <div className="flex items-center justify-between"> <EditButton id={item.id} /> <DeleteButton id={item.id}/> </div> </div> ))} </div> </div> ); }components\button.tsx
//components\button.tsx "use client"; import { useFormStatus } from "react-dom"; import { clsx } from "clsx"; import { deleteData } from "@/lib/actions"; import Link from "next/link"; export const SubmitButton = ({ label }: { label: string }) => { const { pending } = useFormStatus(); return ( <button className={clsx( "bg-blue-700 text-white w-full font-medium py-2.5 px-6 text-base rounded-sm hover:bg-blue-600", { "opacity-50 cursor-progress": pending, } )} type="submit" disabled={pending} > {label === "upload" ? ( <>{pending ? "Uploading..." : "Upload"}</> ) : ( <>{pending ? "Updating..." : "Update"}</> )} </button> ); }; export const EditButton = ({ id }: { id: string }) => { return ( <Link href={`edit/${id}`} className="py-3 text-sm text-white bg-blue-700 rounded-bl-md w-full hover:bg-blue-800 text-center" > Edit </Link> ); }; export const DeleteButton = ({ id }: { id: string }) => { const deleteDataWithId = deleteData.bind(null, id); return ( <form action={deleteDataWithId} className="py-3 text-sm text-white bg-red-700 rounded-br-md w-full hover:bg-red-800 text-center" > <DeleteBtn /> </form> ); }; const DeleteBtn = () => { const { pending } = useFormStatus(); return ( <button type="submit" disabled={pending}> {pending ? "Deleting..." : "Delete"} </button> ); };lib\data.ts
//lib\data.ts import { prisma } from "@/lib/prisma"; export const getImages = async () => { try { const result = await prisma.upload.findMany({ orderBy: { createdAt: "desc" }, }); return result; } catch (error) { throw new Error("Failed to fetch data"); } }; export const getDataById = async (id: string) => { try { const result = await prisma.upload.findUnique({ where: { id }, }); return result; } catch (error) { throw new Error("Failed to fetch data"); } };lib\data.ts
//lib\prisma.ts import { PrismaClient } from "@prisma/client"; declare global { var prisma: PrismaClient | undefined; } export const prisma = globalThis.prisma || new PrismaClient(); if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;lib\actions.ts
//lib\actions.ts "use server"; import { z } from "zod"; //https://www.npmjs.com/package/zod import { prisma } from "@/lib/prisma"; import { writeFile } from "fs/promises"; import path from "path"; import { redirect } from 'next/navigation' import { revalidatePath } from 'next/cache' import { getDataById } from "@/lib/data"; import fs from 'fs'; const UploadSchema = z.object({ title: z.string().min(1), image: z .instanceof(File) .refine((file) => file.size > 0, { message: "Image is required" }) .refine((file) => file.size === 0 || file.type.startsWith("image/"), { message: "Only images are allowed", }) .refine((file) => file.size < 4000000, { message: "Image must less than 4MB", }), }); const EditSchema = z.object({ title: z.string().min(1), image: z .instanceof(File) .refine((file) => file.size === 0 || file.type.startsWith("image/"), { message: "Only images are allowed", }) .refine((file) => file.size < 4000000, { message: "Image must less than 4MB", }) .optional(), }); export const CreateData = async (prevState: unknown, formData: FormData) => { const validatedFields = UploadSchema.safeParse( Object.fromEntries(formData.entries()) ); if (!validatedFields.success) { return { error: validatedFields.error.flatten().fieldErrors, }; } const file = formData.get("image"); const { title } = validatedFields.data; try { const buffer = Buffer.from(await file.arrayBuffer()); const filename = file.name.replaceAll(" ", "_"); console.log(filename); await writeFile( path.join(process.cwd(), "public/assets/" + filename), buffer ); await prisma.upload.create({ data: { title, image: filename, }, }); //return { message: "Success" }; } catch (error) { console.log("Error occured ", error); return { message: "Failed" }; } revalidatePath("/"); redirect("/"); }; // Update export const updateData = async ( id: string, prevState: unknown, formData: FormData ) => { const validatedFields = EditSchema.safeParse( Object.fromEntries(formData.entries()) ); if (!validatedFields.success) { return { error: validatedFields.error.flatten().fieldErrors, }; } const data = await getDataById(id); if (!data) return { message: "No Data Found" }; const file = formData.get("image"); const { title, image } = validatedFields.data; let imageFilename; if (!image || image.size <= 0) { imageFilename = data.image; } else { console.log(data.image); fs.unlink("public/assets/" + data.image, (err) => { if (err) { console.error('An error occurred:', err); } else { console.log('File deleted successfully!'); } }); const buffer = Buffer.from(await file.arrayBuffer()); const filename = file.name.replaceAll(" ", "_"); console.log(filename); await writeFile( path.join(process.cwd(), "public/assets/" + filename), buffer ); imageFilename = filename; } try { await prisma.upload.update({ data: { title, image: imageFilename, }, where: { id }, }); } catch (error) { return { message: "Failed to update data" }; } revalidatePath("/"); redirect("/"); }; export const deleteData = async (id: string) => { const data = await getDataById(id); if (!data) return { message: "No data found" }; console.log(data.image); try { fs.unlink("public/assets/" + data.image, (err) => { if (err) { console.error('An error occurred:', err); } else { console.log('File deleted successfully!'); } }); await prisma.upload.delete({ where: { id }, }); } catch (error) { return { message: "Failed to delete data" }; } revalidatePath("/"); };app\create\page.tsx
//app\create\page.tsx import UploadForm from "@/components/upload-form"; const UploadPage = () => { return ( <div className="min-h-screen flex items-center justify-center bg-slate-100"> <div className="bg-white rounded-sm shadow p-8"> <h1 className="text-2xl font-bold mb-5">Upload Image</h1> <UploadForm /> </div> </div> ); }; export default UploadPage;components\upload-form.tsx
//components\upload-form.tsx "use client"; import React from "react"; import { CreateData } from "@/lib/actions"; import { useFormState } from "react-dom"; import { SubmitButton } from "@/components/button"; const UploadForm = () => { const [state, formAction] = useFormState(CreateData, null); return ( <form action={formAction}> {/* Alert */} {state?.message ? ( <div className="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50" role="alert" > <div className="font-medium">{state?.message}</div> </div> ) : null} <div className="mb-4 pt-2"> <input type="text" name="title" className="py-2 px-4 rounded-sm border border-gray-400 w-full" placeholder="Title..." /> <div aria-live="polite" aria-atomic="true"> <p className="text-sm text-red-500 mt-2">{state?.error?.title}</p> </div> </div> <div className="mb-4 pt-2"> <input type="file" name="image" className="file:py-2 file:px-4 file:mr-4 file:rounded-sm file:border-0 file:bg-gray-200 hover:file:bg-gray-300 file:cursor-pointer border border-gray-400 w-full" /> <div aria-live="polite" aria-atomic="true"> <p className="text-sm text-red-500 mt-2">{state?.error?.image}</p> </div> </div> <div className="mb-4 pt-4"> <SubmitButton label="upload" /> </div> </form> ); }; export default UploadForm;app\edit\[id]\page.tsx
//app\edit\[id]\page.tsx import EditForm from "@/components/edit-form"; import { getDataById } from "@/lib/data"; import { notFound } from "next/navigation"; const EditPage = async ({ params }: { params: { id: string } }) => { const data = await getDataById(params.id); if (!data) return notFound(); return ( <div className="min-h-screen flex items-center justify-center bg-slate-100"> <div className="bg-white rounded-sm shadow p-8"> <h1 className="text-2xl font-bold mb-5">Update Image</h1> <EditForm data={data} /> </div> </div> ); }; export default EditPage;components\edit-form.tsx
//components\edit-form.tsx "use client"; import React from "react"; import { updateData } from "@/lib/actions"; import { useFormState } from "react-dom"; import { SubmitButton } from "@/components/button"; import type { Upload } from "@prisma/client"; import Image from "next/image"; const EditForm = ({ data }: { data: Upload }) => { const [state, formAction] = useFormState( updateData.bind(null, data.id), null ); return ( <form action={formAction}> {state?.message ? ( <div className="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50" role="alert" > <div className="font-medium">{state?.message}</div> </div> ) : null} <div className="mb-4 pt-2"> <input type="text" name="title" className="py-2 px-4 rounded-sm border border-gray-400 w-full" placeholder="Title..." defaultValue={data.title} /> <div aria-live="polite" aria-atomic="true"> <p className="text-sm text-red-500 mt-2">{state?.error?.title}</p> </div> <Image src={`http://localhost:3000/assets/${data.image}`} alt={data.title} width={200} height={200} /> </div> <div className="mb-4 pt-2"> <input type="file" name="image" className="file:py-2 file:px-4 file:mr-4 file:rounded-sm file:border-0 file:bg-gray-200 hover:file:bg-gray-300 file:cursor-pointer border border-gray-400 w-full" /> <div aria-live="polite" aria-atomic="true"> <p className="text-sm text-red-500 mt-2">{state?.error?.image}</p> </div> </div> <div className="mb-4 pt-4"> <SubmitButton label="update" /> </div> </form> ); }; export default EditForm;prisma\schema.prisma
//prisma\schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = "postgresql://postgres:admin@localhost:5432/postgresDB?schema=public" } model Upload { id String @id @default(cuid()) title String image String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }.env DATABASE_URL="postgresql://postgres:admin@localhost:5432/postgresDB?schema=public"
run C:\nextjs>npm run dev