├── .github ├── renovate.json └── workflows │ └── test.yml ├── .gitignore ├── README.md ├── lib └── prisma.js ├── package.json ├── pages ├── _app.js ├── api │ ├── index.js │ ├── posts.js │ ├── seed.js │ └── users.js └── index.js ├── prisma ├── migrations │ ├── 20211004163457_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── favicon.png ├── index.html ├── prisma.svg └── vercel.svg └── styles ├── Home.module.css └── globals.css /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "reviewers": ["janpio", "ruheni", "zachtil"], 4 | "semanticCommits": "enabled", 5 | "dependencyDashboard": true, 6 | "timezone": "Europe/Berlin", 7 | "baseBranches": ["main", "data-proxy"], 8 | "packageRules": [ 9 | { 10 | "matchPackageNames": ["next", "react", "react-dom"], 11 | "groupName": "deps (non-major)", 12 | "automerge": "true" 13 | }, 14 | { 15 | "matchPackagePatterns": ["@prisma/*"], 16 | "matchPackageNames": ["prisma"], 17 | "matchUpdateTypes": ["minor", "patch"], 18 | "groupName": "Prisma Dependencies", 19 | "groupSlug": "prisma-deps", 20 | "automerge": "true" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | - data-proxy 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | services: 12 | postgres: 13 | image: postgres 14 | env: 15 | POSTGRES_USER: postgres 16 | POSTGRES_PASSWORD: postgres 17 | options: >- 18 | --health-cmd pg_isready 19 | --health-interval 10s 20 | --health-timeout 5s 21 | --health-retries 5 22 | ports: 23 | - 5432:5432 24 | env: 25 | DATABASE_URL: postgresql://postgres:postgres@localhost:5432/deployment-example-vercel 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 16 31 | - run: npm install 32 | - run: npx prisma migrate deploy 33 | - name: 'Start server & test API routes' 34 | run: | 35 | npm run dev & 36 | pid=$! 37 | 38 | sleep 15 39 | # check api routes 40 | curl --fail 'http://localhost:3000/api' 41 | curl --fail 'http://localhost:3000/api/seed' 42 | curl --fail 'http://localhost:3000/api/users' 43 | curl --fail 'http://localhost:3000/api/posts' 44 | kill "$pid" 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .now 2 | node_modules/ 3 | *.env* 4 | 5 | .vercel 6 | package-lock.json 7 | 8 | # next.js 9 | /.next/ 10 | /out/ 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | *.pem 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # local env files 25 | .env.local 26 | .env.development.local 27 | .env.test.local 28 | .env.production.local 29 | 30 | # vercel 31 | .vercel 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vercel deployment example 2 | 3 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fprisma%2Fdeployment-example-vercel&env=DATABASE_URL&envDescription=PostgreSQL%20connection%20string&envLink=https%3A%2F%2Fwww.prisma.io%2Fdocs%2Fconcepts%2Fdatabase-connectors%2Fpostgresql%23connection-url&project-name=prisma-vercel-deployment-example&repo-name=prisma-vercel-deployment-example) 4 | 5 | [Deployment guide](https://www.prisma.io/docs/guides/deployment/deploying-to-vercel) -------------------------------------------------------------------------------- /lib/prisma.js: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | // Avoid instantiating too many instances of Prisma in development 4 | // https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices#problem 5 | let prisma 6 | 7 | if (process.env.NODE_ENV === 'production') { 8 | prisma = new PrismaClient() 9 | } else { 10 | if (!global.prisma) { 11 | global.prisma = new PrismaClient() 12 | } 13 | prisma = global.prisma 14 | } 15 | 16 | export default prisma 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deployment-example-prisma-vercel", 3 | "dependencies": { 4 | "@prisma/client": "4.16.2", 5 | "next": "13.5.11", 6 | "react": "18.3.1", 7 | "react-dom": "18.3.1" 8 | }, 9 | "scripts": { 10 | "dev": "next dev", 11 | "build": "next build", 12 | "start": "next start", 13 | "vercel-build": "prisma generate && prisma migrate deploy && next build", 14 | "prisma:generate": "prisma generate" 15 | }, 16 | "devDependencies": { 17 | "prisma": "4.16.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /pages/api/index.js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | res.status(200).json({ up: true }) 3 | } 4 | -------------------------------------------------------------------------------- /pages/api/posts.js: -------------------------------------------------------------------------------- 1 | import prisma from '../../lib/prisma' 2 | 3 | export default async (req, res) => { 4 | if (req.method === 'GET') { 5 | try { 6 | const users = await prisma.post.findMany({ 7 | include: { author: true }, 8 | }) 9 | res.status(200).json(users) 10 | } catch (error) { 11 | console.error(error) 12 | res.status(500).json(error) 13 | } 14 | } else if (req.method === 'POST') { 15 | const { title, content, authorEmail } = req.body 16 | try { 17 | const createdPost = await prisma.post.create({ 18 | data: { 19 | title, 20 | content, 21 | author: { 22 | connect: { 23 | email: authorEmail, 24 | }, 25 | }, 26 | }, 27 | }) 28 | res.status(200).json(createdPost) 29 | } catch (e) { 30 | console.error(e) 31 | return res.status(500) 32 | } 33 | } else { 34 | res.status(404) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/api/seed.js: -------------------------------------------------------------------------------- 1 | import prisma from '../../lib/prisma' 2 | 3 | export default async (req, res) => { 4 | try { 5 | await Promise.all([ 6 | prisma.profile.deleteMany({}), 7 | prisma.post.deleteMany({}), 8 | ]) 9 | await prisma.user.deleteMany({}) 10 | 11 | const createdUser = await prisma.user.create({ 12 | data: seedUser, 13 | }) 14 | 15 | const createdUser2 = await prisma.user.create({ 16 | data: seedUser2, 17 | }) 18 | 19 | res.status(201).json([createdUser, createdUser2]) 20 | } catch (error) { 21 | console.error(error) 22 | return res.status(500).end() 23 | } 24 | } 25 | 26 | const seedUser = { 27 | email: 'jane@prisma.io', 28 | name: 'Jane', 29 | profiles: { 30 | create: [ 31 | { 32 | bio: 'Technical Writer', 33 | }, 34 | { 35 | bio: 'Health Enthusiast', 36 | }, 37 | { 38 | bio: 'Self Quantifier', 39 | }, 40 | ], 41 | }, 42 | posts: { 43 | create: [ 44 | { 45 | title: 46 | 'Comparing Database Types: How Database Types Evolved to Meet Different Needs', 47 | content: 48 | 'https://www.prisma.io/blog/comparison-of-database-models-1iz9u29nwn37/', 49 | }, 50 | { 51 | title: 'Analysing Sleep Patterns: The Quantified Self', 52 | content: 'https://quantifiedself.com/get-started/', 53 | }, 54 | ], 55 | }, 56 | } 57 | 58 | const seedUser2 = { 59 | email: 'toru@prisma.io', 60 | name: 'Toru Takemitsu', 61 | profiles: { 62 | create: [ 63 | { 64 | bio: 'Composer', 65 | }, 66 | { 67 | bio: 'Musician', 68 | }, 69 | { 70 | bio: 'Writer', 71 | }, 72 | ], 73 | }, 74 | posts: { 75 | create: [ 76 | { 77 | title: 'Requiem for String Orchestra', 78 | content: '', 79 | }, 80 | { 81 | title: 'Music of Tree', 82 | content: '', 83 | }, 84 | { 85 | title: 'Waves for clarinet, horn, two trombones and bass drum ', 86 | content: '', 87 | }, 88 | ], 89 | }, 90 | } 91 | -------------------------------------------------------------------------------- /pages/api/users.js: -------------------------------------------------------------------------------- 1 | import prisma from '../../lib/prisma' 2 | import { Prisma } from '@prisma/client' 3 | 4 | export default async (req, res) => { 5 | if (req.method === 'GET') { 6 | try { 7 | const users = await prisma.user.findMany({ 8 | include: { profiles: true }, 9 | }) 10 | res.status(200).json(users) 11 | } catch (error) { 12 | console.error(error) 13 | res.status(500).json(error) 14 | } 15 | } else if (req.method === 'POST') { 16 | try { 17 | const createdUser = await prisma.user.create({ 18 | data: req.body, 19 | }) 20 | res.status(200).json(createdUser) 21 | } catch (e) { 22 | if (e instanceof Prisma.PrismaClientKnownRequestError) { 23 | if (e.code === 'P2002') { 24 | return res 25 | .status(409) 26 | .json({ error: 'A user with this email already exists' }) 27 | } 28 | } 29 | console.error(e) 30 | return res.status(500) 31 | } 32 | } else { 33 | res.status(404) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import styles from '../styles/Home.module.css' 3 | import { useCallback, useState } from 'react' 4 | 5 | const fetchApi = (endpoint) => { 6 | return fetch(`/api/${endpoint}`).then((response) => { 7 | if (!response.ok) { 8 | throw new Error('Network response was not ok') 9 | } 10 | return response.json() 11 | }) 12 | } 13 | 14 | export default function Home() { 15 | const [isLoadingPost, setLoadingPost] = useState(false) 16 | const [apiResponse, setApiResponse] = useState(null) 17 | const [apiError, setApiError] = useState(null) 18 | 19 | const getApiCallback = useCallback( 20 | (endpoint) => async (e) => { 21 | setLoadingPost(true) 22 | setApiError(null) 23 | try { 24 | const response = await fetchApi(endpoint) 25 | setApiResponse(response) 26 | } catch (e) { 27 | setApiError(e) 28 | console.error(e) 29 | } 30 | setLoadingPost(false) 31 | }, 32 | [], 33 | ) 34 | 35 | const onGetStatus = useCallback(getApiCallback(''), []) 36 | const onSeed = useCallback(getApiCallback('seed'), []) 37 | const onGetUsers = useCallback(getApiCallback('users'), []) 38 | const onGetPosts = useCallback(getApiCallback('posts'), []) 39 | 40 | return ( 41 |
42 | 43 | Prisma example with Vercel 44 | 45 | 46 | 47 |
48 |

Prisma Vercel Deployment Example

49 | 50 |
51 | 54 | 57 | 60 | 63 |
66 |
67 |
72 |           {apiResponse && JSON.stringify(apiResponse, null, 2)}
73 |         
74 |
75 | 76 |
77 | Powered by{' '} 78 | Vercel Logo 79 | & 80 | Prisma Logo 81 |
82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /prisma/migrations/20211004163457_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Post" ( 3 | "id" SERIAL NOT NULL, 4 | "content" TEXT, 5 | "title" TEXT NOT NULL, 6 | "authorId" INTEGER, 7 | 8 | CONSTRAINT "Post_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- CreateTable 12 | CREATE TABLE "Profile" ( 13 | "id" SERIAL NOT NULL, 14 | "bio" TEXT, 15 | "userId" INTEGER NOT NULL, 16 | 17 | CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateTable 21 | CREATE TABLE "User" ( 22 | "id" SERIAL NOT NULL, 23 | "email" TEXT NOT NULL, 24 | "name" TEXT, 25 | 26 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 27 | ); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 37 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Post { 11 | id Int @id @default(autoincrement()) 12 | content String? 13 | title String 14 | authorId Int? 15 | author User? @relation(fields: [authorId], references: [id]) 16 | } 17 | 18 | model Profile { 19 | id Int @id @default(autoincrement()) 20 | bio String? 21 | userId Int 22 | user User @relation(fields: [userId], references: [id]) 23 | } 24 | 25 | model User { 26 | id Int @id @default(autoincrement()) 27 | email String @unique 28 | name String? 29 | posts Post[] 30 | profiles Profile[] 31 | } 32 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prisma/deployment-example-vercel/545e7ddf088cb034461e59eabeeabaeab4503edd/public/favicon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Prisma example with Vercel 5 | 9 | 53 | 54 | 55 |
56 |
57 |

58 | Prisma example with Vercel 59 |

60 | 66 | 72 | 78 | 84 | 85 |
86 |
87 | 88 |
89 | 92 |
93 | 94 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /public/prisma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 5rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | height: 100px; 22 | border-top: 1px solid #eaeaea; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | 28 | .footer img { 29 | margin-left: 0.5rem; 30 | } 31 | 32 | .footer a { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .title a { 39 | color: #0070f3; 40 | text-decoration: none; 41 | } 42 | 43 | .title a:hover, 44 | .title a:focus, 45 | .title a:active { 46 | text-decoration: underline; 47 | } 48 | 49 | .title { 50 | margin: 0; 51 | line-height: 1.15; 52 | font-size: 3rem; 53 | } 54 | 55 | .title, 56 | .description { 57 | text-align: center; 58 | } 59 | 60 | .description { 61 | line-height: 1.5; 62 | font-size: 1.5rem; 63 | } 64 | 65 | .code { 66 | background: #fafafa; 67 | border-radius: 5px; 68 | padding: 0.75rem; 69 | font-size: 1.1rem; 70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 71 | Bitstream Vera Sans Mono, Courier New, monospace; 72 | } 73 | 74 | .grid { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-wrap: wrap; 79 | max-width: 400px; 80 | margin-top: 3rem; 81 | } 82 | 83 | .card { 84 | margin: 0.5rem; 85 | flex-basis: 45%; 86 | padding: 1rem; 87 | text-align: left; 88 | color: inherit; 89 | text-decoration: none; 90 | border: 1px solid #eaeaea; 91 | border-radius: 10px; 92 | transition: color 0.15s ease, border-color 0.15s ease; 93 | } 94 | 95 | 96 | .apiButton { 97 | margin: 0.5rem; 98 | flex-basis: 45%; 99 | padding: 1rem; 100 | color: white; 101 | background-color: #0152ae; 102 | border: none; 103 | padding: 10px 15px; 104 | border-radius: 10px; 105 | cursor: pointer; 106 | } 107 | 108 | .apiButton:hover, 109 | .apiButton:focus, 110 | .apiButton:active { 111 | outline: none; 112 | } 113 | 114 | .apiButton:hover { 115 | background-color: #0071f2; 116 | } 117 | 118 | .logo { 119 | height: 1em; 120 | margin-right: 0.5rem; 121 | } 122 | 123 | @media (max-width: 600px) { 124 | .grid { 125 | width: 100%; 126 | flex-direction: column; 127 | } 128 | } 129 | 130 | .loader, 131 | .loader:after { 132 | border-radius: 50%; 133 | width: 10em; 134 | height: 10em; 135 | } 136 | .loader { 137 | top: 30px; 138 | right: 30px; 139 | font-size: 3px; 140 | position: fixed; 141 | text-indent: -9999em; 142 | border-top: 1.1em solid rgb(59, 177, 255); 143 | border-right: 1.1em solid rgb(59, 177, 255); 144 | border-bottom: 1.1em solid rgb(59, 177, 255); 145 | border-left: 1.1em solid #ffffff; 146 | -webkit-transform: translateZ(0); 147 | -ms-transform: translateZ(0); 148 | transform: translateZ(0); 149 | -webkit-animation: load8 1.1s infinite linear; 150 | animation: load8 1.1s infinite linear; 151 | } 152 | @-webkit-keyframes load8 { 153 | 0% { 154 | -webkit-transform: rotate(0deg); 155 | transform: rotate(0deg); 156 | } 157 | 100% { 158 | -webkit-transform: rotate(360deg); 159 | transform: rotate(360deg); 160 | } 161 | } 162 | @keyframes load8 { 163 | 0% { 164 | -webkit-transform: rotate(0deg); 165 | transform: rotate(0deg); 166 | } 167 | 100% { 168 | -webkit-transform: rotate(360deg); 169 | transform: rotate(360deg); 170 | } 171 | } 172 | 173 | .hidden { 174 | display: none 175 | } 176 | 177 | .code { 178 | background-color: black; 179 | color: white; 180 | max-height: 400px; 181 | overflow: scroll; 182 | font-size: 1rem; 183 | } -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | --------------------------------------------------------------------------------