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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | //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> ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | //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> ); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //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" ); } }; |
1 2 3 4 5 6 7 8 9 10 | //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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | //lib\actions.ts "use server" ; 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( "/" ); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | //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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | //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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //prisma\schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" } model Upload { id String @id @ default (cuid()) title String image String createdAt DateTime @ default (now()) updatedAt DateTime @updatedAt } |
run C:\nextjs>npm run dev