├── .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 | [](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 |
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 |
93 |
94 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/public/prisma.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------