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
