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
