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 Employee {
id String @id @default(cuid())
name String
email String
phone 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/react-suspense/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 | //app/react-suspense/page.tsx import Link from "next/link" ; import TableData from "@/components/employeetable" ; import { Suspense } from "react" ; import { Spinner } from "@/components/spinner" ; export default function Home() { return ( <div className= "w-screen py-20 flex justify-center flex-col items-center" > <div className= "flex items-center justify-between gap-1 mb-5" > <h1 className= "text-4xl font-bold" >Next.js 14 How to use React Suspense for data loading <br/>Prisma PostgreSQL | TailwindCSS DaisyUI</h1> </div> <div className= "overflow-x-auto" > <div className= "mb-2 w-full text-right" > <Link href= "/react-suspense/create" className= "btn btn-primary" > Create </Link> </div> <Suspense fallback={<Spinner />}> <TableData /> </Suspense> </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 62 63 64 65 66 67 68 | //app/react-suspense/create/page.tsx "use client" ; import { useFormState } from "react-dom" ; import { saveEmployee } from "@/lib/action" ; const CreateEmployeePage = () => { const [state, formAction] = useFormState(saveEmployee, null); return ( <div className= "max-w-md mx-auto mt-5" > <h1 className= "text-2xl text-center mb-2" >Add New Employee</h1> <div> <form action={formAction}> <div className= "mb-5" > <label htmlFor= "name" className= "block text-sm font-medium text-gray-900" > Full Name </label> <input type= "text" name= "name" id= "name" className= "input input-bordered input-primary w-full max-w-xs" placeholder= "Full Name..." /> <div id= "name-error" aria-live= "polite" aria-atomic= "true" > <p className= "mt-2 text-sm text-red-500" >{state?.Error?.name}</p> </div> </div> <div className= "mb-5" > <label htmlFor= "email" className= "block text-sm font-medium text-gray-900" > Email </label> <input type= "email" name= "email" id= "email" className= "input input-bordered input-primary w-full max-w-xs" placeholder= "Email..." /> <div id= "email-error" aria-live= "polite" aria-atomic= "true" > <p className= "mt-2 text-sm text-red-500" >{state?.Error?.email}</p> </div> </div> <div className= "mb-5" > <label htmlFor= "phone" className= "block text-sm font-medium text-gray-900" > Phone Number </label> <input type= "text" name= "phone" id= "phone" className= "input input-bordered input-primary w-full max-w-xs" placeholder= "Phone Number..." /> <div id= "phone-error" aria-live= "polite" aria-atomic= "true" > <p className= "mt-2 text-sm text-red-500" >{state?.Error?.phone}</p> </div> </div> <button className= "btn btn-primary" >Save</button> </form> </div> </div> ); }; export default CreateEmployeePage; |
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 | //components\employeetable.tsx import { getEmployeelist } from "@/lib/action" ; import { formatDate } from "@/lib/utils" ; const TableEmployee = async ({ query, }: { query: string; }) => { const employees = await getEmployeelist(query); return ( <table className= "table table-zebra" > <thead className= "text-sm text-gray-700 uppercase bg-gray-50" > <tr> <th className= "py-3 px-6" >#</th> <th className= "py-3 px-6" >Name</th> <th className= "py-3 px-6" >Email</th> <th className= "py-3 px-6" >Phone Number</th> <th className= "py-3 px-6" >Created At</th> <th className= "py-3 px-6 text-center" >Actions</th> </tr> </thead> <tbody> {employees.map((rs, index) => ( <tr key={rs.id} className= "bg-white border-b" > <td className= "py-3 px-6" >{index + 1}</td> <td className= "py-3 px-6" >{rs.name}</td> <td className= "py-3 px-6" >{rs.email}</td> <td className= "py-3 px-6" >{rs.phone}</td> <td className= "py-3 px-6" > date format </td> <td className= "flex justify-center gap-1 py-3" > <button className= "btn btn-info" >View</button> <button className= "btn btn-success" >Edit</button> <button className= "btn btn-warning" > Delete </button> </td> </tr> ))} </tbody> </table> ); }; export default TableEmployee; |
1 2 3 4 5 6 | //components\spinner.tsx export const Spinner = () => { return ( <span className= "loading loading-spinner loading-lg" ></span> ); }; |
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 | //lib/action.ts "use server" ; import { prisma } from "@/lib/prisma" ; import { revalidatePath } from "next/cache" ; import { redirect } from "next/navigation" ; const EmployeeSchema = z.object({ name: z.string().min(6), email: z.string().min(6), phone: z.string().min(11), }); export const saveEmployee = async (prevSate: any, formData: FormData) => { const validatedFields = EmployeeSchema.safeParse( Object.fromEntries(formData.entries()) ); if (!validatedFields.success) { return { Error: validatedFields.error.flatten().fieldErrors, }; } try { await prisma.employee.create({ data: { name: validatedFields.data.name, email: validatedFields.data.email, phone: validatedFields.data.phone, }, }); } catch (error) { return { message: "Failed to create new employee" }; } revalidatePath( "/react-suspense" ); redirect( "/react-suspense" ); }; export const getEmployeelist = async (query: string) => { try { await new Promise((resolve) => setTimeout(resolve, 2000)); const employees = await prisma.employee.findMany({ select: { id: true, name: true, email: true, phone: true, createdAt: true, }, orderBy: { createdAt: "desc" , }, }); return employees; } catch (error) { throw new Error( "Failed to fetch employees 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 | //prisma\schema.prisma // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env( "DATABASE_URL" ) } model Employee { id String @id @ default (cuid()) name String email String phone String createdAt DateTime @ default (now()) updatedAt DateTime @updatedAt } |
run C:\nextjs>npm run dev