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 Post {
id Int @id @default(autoincrement())
title String
body 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
install Kinde NextJS
npm i @kinde-oss/kinde-auth-nextjs
https://www.npmjs.com/package/@kinde-oss/kinde-auth-nextjs
https://kinde.notion.site/Next-js-App-Router-v2-e7a16d8ae38e45b6ad052910075e24ef
https://kinde.com/
app\page.tsx
//app\page.tsx import prisma from "@/lib/db"; import { Suspense } from "react"; import Image from "next/image"; export default async function Home() { //export default function Home() { await new Promise((resolve) => setTimeout(resolve, 1000)); const posts = await prisma.post.findMany(); return ( <main className="text-center pt-32 px-5"> <h1 className="text-4xl md:text-5xl font-bold mb-5"> Our Blog </h1> <Suspense fallback="Loading..."> <ul> {posts.map((post) => ( <li key={post.id} className="mb-3"> <div className="card lg:card-side bg-base-100 shadow-xl"> <figure> <Image src="https://img.daisyui.com/images/stock/photo-1494232410401-ad00d5433cfa.jpg" alt="title" width="305" height="305" /> </figure> <div className="card-body"> <h2 className="card-title">{post.title}</h2> <p>{post.body}</p> <div className="card-actions justify-end"> <button className="btn btn-primary">Details</button> </div> </div> </div> </li> ))} </ul> </Suspense> </main> ); }app\layout.tsx
//app\layout.tsx import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Header from "@/components/header"; import Footer from "@/components/footer"; import Container from "@/components/container"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "My Posts", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body> <Container> <Header /> {children} <Footer /> </Container> </body> </html> ); }components\container.tsx
//components\container.tsx export default function Container({ children }: { children: React.ReactNode }) { return ( <div className="max-w-[1100px] mx-auto bg-white min-h-screen flex flex-col"> {children} </div> ); }components\delete.tsx
//components\delete.tsx import { deletePost } from "@/actions/actions"; export const DeleteButton = ({ id }: { id: string }) => { const DeletedeletePostWithId = deletePost.bind(null, id); return ( <form action={DeletedeletePostWithId}> <button className="btn btn-error"> Delete </button> </form> ); };components\editform.tsx
//components\editform.tsx "use client"; import { updatePost } from "@/actions/actions"; import { useFormState } from "react-dom"; import type { Post } from "@prisma/client"; const UpdateForm = ({ post }: { post: Post }) => { const UpdatePostWithId = updatePost.bind(null, post.id); const [state, formAction] = useFormState(UpdatePostWithId, null); return ( <div className="flex flex-col max-w-[400px] mx-auto gap-2 my-10"> <form action={formAction}> <div className="w-full"> <label htmlFor="name" className="block text-sm font-medium text-gray-900"> Title </label> <input type="text" name="title" id="title" className="input input-bordered input-primary w-full max-w-xs" placeholder="Title..." defaultValue={post.title} /> <div id="name-error" aria-live="polite" aria-atomic="true"> <p className="mt-2 text-sm text-red-500">{state?.Error?.title}</p> </div> </div> <div className="w-full"> <label htmlFor="name" className="block text-sm font-medium text-gray-900"> Description </label> <textarea name="body" id="body" placeholder="Body content for new post" className="textarea textarea-primary textarea-xs w-full max-w-xs" rows={6} defaultValue={post.body} required /> <div id="name-error" aria-live="polite" aria-atomic="true"> <p className="mt-2 text-sm text-red-500">{state?.Error?.body}</p> </div> </div> <div id="message-error" aria-live="polite" aria-atomic="true"> <p className="mt-2 text-sm text-red-500">{state?.message}</p> </div> <button className="btn btn-primary">Update</button> </form> </div> ); }; export default UpdateForm;components\footer.tsx
//components\footer.tsx export default function Footer() { return ( <footer className="mt-auto text-center text-zinc-400 py-5 px-7"> <small>All rights reserved.</small> </footer> ); }components\form.tsx
//components\form.tsx import { createPost } from "@/actions/actions"; import React from "react"; export default function Form() { return ( <form action={createPost} className="flex flex-col max-w-[400px] mx-auto gap-2 my-10" > <input type="text" name="title" placeholder="Title for new post" className="border rounded px-3 h-10" required /> <textarea name="body" placeholder="Body content for new post" className="border rounded px-3 py-2" rows={6} required /> <button className="h-10 bg-blue-500 px-5 rounded text-white"> Submit </button> </form> ); }components\header.tsx
//components\header.tsx "use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; const navLinks = [ { href: "/", label: "Home", }, { href: "/posts", label: "Admin", }, ]; export default function Header() { const pathname = usePathname(); return ( <header className="flex justify-between items-center py-4 px-7"> <Link href="https://www.youtube.com/@cairocoders"> Cairocoders </Link> <nav> <ul className="flex gap-x-5 text-[14px]"> {navLinks.map((link) => ( <li key={link.href}> <Link className={`${ pathname === link.href ? "text-zinc-900" : "text-zinc-400" }`} href={link.href} > {link.label} </Link> </li> ))} </ul> </nav> </header> ); }components\posts-list.tsx
//components\posts-list.tsx import prisma from "@/lib/db"; import Link from "next/link"; import { DeleteButton } from "@/components/delete"; export default async function PostsList() { await new Promise((resolve) => setTimeout(resolve, 1000)); const posts = await prisma.post.findMany(); 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">Title</th> <th className="py-3 px-6">Description</th> <th className="py-3 px-6 text-center">Actions</th> </tr> </thead> <tbody> {posts.map((post) => ( <tr key={post.id} className="bg-white border-b"> <td className="py-3 px-6">{post.id}</td> <td className="py-3 px-6">{post.title}</td> <td className="py-3 px-6">{post.body}</td> <td className="flex justify-center gap-1 py-3"> <button className="btn btn-info"> <Link href={`/posts/${post.id}`}>View</Link> </button> <Link href={`/edit/${post.id}`} className="btn btn-info" > Edit </Link> <DeleteButton id={post.id} /> </td> </tr> ))} </tbody> </table> ); }lib\db.ts
//lib\db.ts import { PrismaClient } from "@prisma/client"; const prismaClientSingleton = () => { return new PrismaClient(); }; declare const globalThis: { prismaGlobal: ReturnType<typeof prismaClientSingleton>; } & typeof global; const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); export default prisma; if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;posts\page.tsx
//posts\page.tsx import PostsList from "@/components/posts-list"; import { Suspense } from "react"; import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; import { redirect } from "next/navigation"; import Link from "next/link"; import { LogoutLink } from "@kinde-oss/kinde-auth-nextjs/components"; export default async function Page() { // auth check const { isAuthenticated } = getKindeServerSession(); if (!(await isAuthenticated())) { redirect("/api/auth/login"); } return ( <main className="text-center pt-16 px-5"> <h1 className="text-4xl md:text-5xl font-bold mb-5">All posts</h1> <span className="btn btn-info"> <Link href="/create-post">Create post</Link> </span> <span className="btn btn-error"><LogoutLink>Log out</LogoutLink></span> <Suspense fallback="Loading..."> <PostsList /> </Suspense> </main> ); }posts\[id]\page.tsx
//posts\[id]\page.tsx import prisma from "@/lib/db"; import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) { const post = await prisma.post.findUnique({ where: { id: parseInt(params.id), }, }); if (!post) { notFound(); } return ( <main className="px-7 pt-24 text-center"> <h1 className="text-5xl font-semibold mb-7">{post.title}</h1> <p className="max-w-[700px] mx-auto">{post.body}</p> </main> ); }create-post\page.tsx
//create-post\page.tsx import Form from "@/components/form"; import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; import { redirect } from "next/navigation"; export default async function Page() { // auth check const { isAuthenticated } = getKindeServerSession(); if (!(await isAuthenticated())) { redirect("/api/auth/login"); } return ( <main className="text-center pt-16"> <h1 className="text-4xl md:text-5xl font-bold mb-5">Create post</h1> <Form /> </main> ); }edit\[id]\page.tsx
//edit\[id]\page.tsx import UpdateForm from "@/components/editform"; import prisma from "@/lib/db"; import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) { console.log(params.id); const post = await prisma.post.findUnique({ where: { id: parseInt(params.id), }, }); if (!post) { notFound(); } return ( <div className="max-w-md mx-auto w-full mt-5"> <h1 className="text-2xl text-center mb-2">Update Post</h1> <UpdateForm post={post}/> </div> ); }api\auth\[kindeAuth]\route.js
//api\auth\[kindeAuth]\route.js import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server"; export const GET = handleAuth();actions\actions.ts
//actions\actions.ts "use server"; import { z } from "zod"; //npm i zod https://www.npmjs.com/package/zod import prisma from "@/lib/db"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; const PostSchema = z.object({ title: z.string().min(3), body: z.string().min(20), }); export async function createPost(formData: FormData) { const title = formData.get("title") as string; const body = formData.get("body") as string; console.log(title); console.log(body); // update database await prisma.post.create({ data: { title, body, }, }); // revalidate revalidatePath("/posts"); } export const updatePost = async ( id: string, prevSate: any, formData: FormData ) => { const validatedFields = PostSchema.safeParse( Object.fromEntries(formData.entries()) ); if (!validatedFields.success) { return { Error: validatedFields.error.flatten().fieldErrors, }; } try { await prisma.post.update({ data: { title: validatedFields.data.title, body: validatedFields.data.body, }, where: { id }, }); } catch (error) { return { message: "Failed to update" }; } revalidatePath("/posts"); redirect("/posts"); }; export const deletePost = async (id: string) => { try { await prisma.post.delete({ where: { id }, }); } catch (error) { return { message: "Failed to delete post" }; } revalidatePath("/posts"); };prisma\schema.prisma
//prisma\schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Post { id Int @id @default(autoincrement()) title String body String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }.env
//.env DATABASE_URL="postgresql://postgres:admin@localhost:5432/postgresDB?schema=public".env.local
//.env.local KINDE_CLIENT_ID=cairocoders KINDE_CLIENT_SECRET=cairocodersednalan KINDE_ISSUER_URL=https://cairocoders.kinde.com KINDE_SITE_URL=http://localhost:3000 KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000 KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/dashboardrun C:\nextjs>npm run dev