├── .eslintrc.json ├── app ├── favicon.ico ├── page.tsx ├── api │ └── posts │ │ └── route.ts ├── layout.tsx └── globals.css ├── components ├── toaster.tsx ├── loading-dots.tsx ├── loading-dots.module.css ├── posts.tsx └── forms.tsx ├── next.config.js ├── postcss.config.js ├── .env.example ├── lib ├── utils.ts ├── prisma.ts └── actions.ts ├── prisma └── schema.prisma ├── .gitignore ├── tailwind.config.js ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json ├── README.md └── pnpm-lock.yaml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steven-tey/prisma-server-actions/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /components/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export { Toaster as default } from "sonner"; 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | experimental: { 3 | serverActions: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # create a free database here: https://vercel.com/docs/storage/vercel-postgres 2 | 3 | POSTGRES_PRISMA_URL= 4 | POSTGRES_URL_NON_POOLING= 5 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let prisma: PrismaClient; 4 | 5 | declare global { 6 | var prisma: PrismaClient | undefined; 7 | } 8 | 9 | if (process.env.NODE_ENV === "production") { 10 | prisma = new PrismaClient(); 11 | } else { 12 | if (!global.prisma) { 13 | global.prisma = new PrismaClient(); 14 | } 15 | prisma = global.prisma; 16 | } 17 | 18 | export default prisma; 19 | -------------------------------------------------------------------------------- /components/loading-dots.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./loading-dots.module.css"; 2 | 3 | const LoadingDots = ({ color = "#000" }: { color?: string }) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default LoadingDots; 14 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // schema.prisma 2 | 3 | generator client { 4 | provider = "prisma-client-js" 5 | } 6 | 7 | datasource db { 8 | provider = "postgresql" 9 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling 10 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection 11 | } 12 | 13 | model Post { 14 | id String @default(cuid()) @id 15 | title String 16 | content String? 17 | published Boolean @default(false) 18 | } -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import Posts, { PostsPlaceholder } from "@/components/posts"; 3 | import { ActionsForm, OldForm } from "@/components/forms"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 9 | {/* */} 10 | }> 11 | 12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /lib/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import prisma from "@/lib/prisma"; 4 | 5 | export async function createPost(data: FormData) { 6 | const title = data.get("title") as string; 7 | const content = data.get("content") as string; 8 | 9 | await prisma.post.create({ 10 | data: { 11 | title, 12 | content, 13 | }, 14 | }); 15 | 16 | return { success: true }; 17 | } 18 | 19 | export async function deletePost(id: string) { 20 | await prisma.post.delete({ 21 | where: { 22 | id, 23 | }, 24 | }); 25 | 26 | return { success: true }; 27 | } 28 | -------------------------------------------------------------------------------- /app/api/posts/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/prisma"; 2 | 3 | export async function POST(req: Request) { 4 | const { title, content } = await req.json(); 5 | 6 | await prisma.post.create({ 7 | data: { 8 | title, 9 | content, 10 | }, 11 | }); 12 | 13 | return { success: true }; 14 | } 15 | 16 | export async function DELETE(req: Request) { 17 | const { searchParams } = new URL(req.url); 18 | const id = searchParams.get("id") as string; 19 | 20 | await prisma.post.delete({ 21 | where: { 22 | id, 23 | }, 24 | }); 25 | 26 | return { success: true }; 27 | } 28 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import Toaster from "@/components/toaster"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Create Next App", 10 | description: "Generated by create next app", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /components/loading-dots.module.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | 6 | .loading .spacer { 7 | margin-right: 2px; 8 | } 9 | 10 | .loading span { 11 | animation-name: blink; 12 | animation-duration: 1.4s; 13 | animation-iteration-count: infinite; 14 | animation-fill-mode: both; 15 | width: 5px; 16 | height: 5px; 17 | border-radius: 50%; 18 | display: inline-block; 19 | margin: 0 1px; 20 | } 21 | 22 | .loading span:nth-of-type(2) { 23 | animation-delay: 0.2s; 24 | } 25 | 26 | .loading span:nth-of-type(3) { 27 | animation-delay: 0.4s; 28 | } 29 | 30 | @keyframes blink { 31 | 0% { 32 | opacity: 0.2; 33 | } 34 | 20% { 35 | opacity: 1; 36 | } 37 | 100% { 38 | opacity: 0.2; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-server-actions", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "prisma generate && prisma db push && next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "5.0.0", 13 | "@types/node": "20.4.5", 14 | "@types/react": "18.2.17", 15 | "@types/react-dom": "18.2.7", 16 | "autoprefixer": "10.4.14", 17 | "clsx": "^2.0.0", 18 | "eslint": "8.45.0", 19 | "eslint-config-next": "13.4.12", 20 | "next": "13.4.12", 21 | "postcss": "8.4.27", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "sonner": "^0.6.2", 25 | "tailwind-merge": "^1.14.0", 26 | "tailwindcss": "3.3.3", 27 | "typescript": "5.1.6" 28 | }, 29 | "devDependencies": { 30 | "prisma": "^5.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/posts.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/prisma"; 2 | import { DeleteButton, OldDeleteButton } from "./forms"; 3 | 4 | export default async function Posts() { 5 | const posts = await prisma.post.findMany(); 6 | 7 | return ( 8 |
9 | {posts.map((post) => ( 10 |
14 |
15 |

{post.title}

16 |

{post.content}

17 |
18 | 19 | {/* */} 20 |
21 | ))} 22 |
23 | ); 24 | } 25 | 26 | export function PostsPlaceholder() { 27 | return ( 28 |
29 | {Array.from({ length: 3 }).map((_, i) => ( 30 |
31 |
32 |
33 |
34 | ))} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prisma Server Actions 2 | 3 | Simple demo app I made for my [Prisma Serverless Webinar](https://www.youtube.com/live/gPyKKD6mODE?feature=share&t=2002) on July 27, 2023 that shows how you can use [Prisma](https://www.prisma.io/) with [Next.js Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions). 4 | 5 | https://github.com/steven-tey/prisma-server-actions/assets/28986134/ce66df21-d43c-4550-a5cd-4572eab44a53 6 | 7 | ## One-click Deploy 8 | 9 | You can deploy this template to Vercel with the button below: 10 | 11 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsteven-tey%2Fprisma-server-actions&project-name=prisma-server-actions&repository-name=prisma-server-actions&demo-title=Prisma+Server+Actions&demo-description=Demo+app+showing+how+you+can+use+Prisma+in+Next.js+Server+Actions&demo-url=https%3A%2F%2Fprisma-server-actions.vercel.app%2F&demo-image=https%3A%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F5ZfVGicwyyTIRtomwbggST%2Fb5fa3f1b302f4d5f4d8302fd3f89295b%2FCleanShot_2023-07-27_at_20.12.35.png%3Fh%3D250&stores=%5B%7B%22type%22%3A%22postgres%22%7D%5D) 12 | 13 | You can also clone & create this repo locally with the following command: 14 | 15 | ```bash 16 | npx create-next-app prisma-server-actions --example "https://github.com/steven-tey/prisma-server-actions" 17 | ``` 18 | -------------------------------------------------------------------------------- /components/forms.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useTransition } from "react"; 4 | import { experimental_useFormStatus as useFormStatus } from "react-dom"; 5 | import { useRouter } from "next/navigation"; 6 | import { createPost, deletePost } from "@/lib/actions"; 7 | import { cn } from "@/lib/utils"; 8 | import LoadingDots from "./loading-dots"; 9 | import { useRef } from "react"; 10 | import { toast } from "sonner"; 11 | 12 | export function ActionsForm() { 13 | const formRef = useRef(null); 14 | const router = useRouter(); 15 | return ( 16 |
19 | createPost(data).then(() => { 20 | toast.success("Post created!"); 21 | formRef.current?.reset(); 22 | router.refresh(); 23 | }) 24 | } 25 | className="flex flex-col bg-white shadow-md rounded-md p-4 w-full max-w-lg space-y-4" 26 | > 27 | 34 |