Prisma is a next-generation ORM that can be used to access a database in Node.js and TypeScript applications. In this guide, you'll learn how to implement a sample fullstack blogging application using the following technologies:
- Next.js as the React framework
- Next.js API Routes for server-side API routes as the backend
- Prisma as the ORM for migrations and database access
- Vercel Postgres as the database
- NextAuth.js for authentication via GitHub (OAuth)
- TypeScript as the programming language
- Vercel for deployment
You'll take advantage of the flexible rendering capabilities of Next.js and at the end, you will deploy the app to Vercel.
Prerequisites
To successfully finish this guide, you'll need:
- Node.js
- A Vercel Account (to set up a free Postgres database and deploy the app)
- A GitHub Account (to create an OAuth app)
Step 1: Set up your Next.js starter project
Navigate into a directory of your choice and run the following command in your terminal to set up a new Next.js project with the pages router:
1npx create-next-app --example https://github.com/prisma/blogr-nextjs-prisma/tree/main blogr-nextjs-prisma
You can now navigate into the directory and launch the app:
1cd blogr-nextjs-prisma && npm run dev
Here's what it looks like at the moment:

The app currently displays hardcoded data that's returned from getStaticProps
in the index.tsx
file. Over the course of the next few sections, you'll change this so that the data is returned from an actual database.
Step 2: Set up your Vercel Postgres database
For the purpose of this guide, we'll use a free Postgres database hosted on Vercel. First, push the repo you cloned in Step 1 to our own GitHub and deploy it to Vercel to create a Vercel project.
Once you have a Vercel project, select the Storage tab, then select the Connect Database button. Under the Create New tab, select Postgres and then the Continue button.
To create a new database, do the following in the dialog that opens:
- Enter
sample_postgres_db
(or any other name you wish) under Store Name. The name can only contain alphanumeric letters, "_" and "-" and can't exceed 32 characters. - Select a region. We recommend choosing a region geographically close to your function region (defaults to US East) for reduced latency.
- Click Create.
Our empty database is created in the region specified. Because you created the Postgres database in a project, we automatically created and added the following environment variables to the project for you.
After running npm i -g vercel@latest
to install the Vercel CLI, pull down the latest environment variables to get your local project working with the Postgres database.
1vercel env pull .env.local
We now have a fully functioning Vercel Postgres database and have all the environment variables to run it locally and on Vercel.
Step 3: Setup Prisma and create the database schema
Next, you will set up Prisma and connect it to your PostgreSQL database. Start by installing the Prisma CLI via npm:
1npm install prisma --save-dev
You'll now create the tables in your database using the Prisma CLI.
To do this, create a prisma folder and add a file called schema.prisma,
your main Prisma configuration file that will contain your database schema.
Add the following model definitions to your schema.prisma
so that it looks like this:
1// schema.prisma2
3generator client {4 provider = "prisma-client-js"5 previewFeatures = ["jsonProtocol"]6}7
8datasource db {9 provider = "postgresql"10 url = env("POSTGRES_PRISMA_URL") // uses connection pooling11 directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection12 shadowDatabaseUrl = env("POSTGRES_URL_NON_POOLING") // used for migrations13}14
15model Post {16 id String @default(cuid()) @id17 title String18 content String?19 published Boolean @default(false)20 author User? @relation(fields: [authorId], references: [id])21 authorId String?22}23
24model User {25 id String @default(cuid()) @id26 name String?27 email String? @unique28 createdAt DateTime @default(now()) @map(name: "created_at")29 updatedAt DateTime @updatedAt @map(name: "updated_at")30 posts Post[]31 @@map(name: "users")32}
Note: You're occasionally using `@map`and`@@map`to map some field and model names to different column and table names in the underlying database. This is because NextAuth.js has some special requirements for calling things in your database a certain way.
This Prisma schema defines two models, each of which will map to a table in the underlying database: User
and Post
. Notice that there's also a relation (one-to-many) between the two models, via the author
field on Post
and the posts
field on User
.
To actually create the tables in your database, you now can use the following command of the Prisma CLI:
1npx prisma db push
You should see the following output:
1Environment variables loaded from /Users/nikolasburk/Desktop/nextjs-guide/blogr-starter/.env.development.local2Prisma schema loaded from prisma/schema.prisma3
4🚀 Your database is now in sync with your schema. Done in 2.10s
Congratulations, the tables have been created! Go ahead and add some initial dummy data using Prisma Studio. Run the following command:
1npx prisma studio
Use Prisma Studio's interface to create a new User
and Post
record and connect them via their relation fields.


Step 4. Install and generate Prisma Client
Before you can access your database from Next.js using Prisma, you first need to install Prisma Client in your app. You can install it via npm as follows:
1npm install @prisma/client
Because Prisma Client is tailored to your own schema, you need to update it every time your Prisma schema file is changing by running the following command:
1npx prisma generate
You'll use a single PrismaClient
instance that you can import into any file where it's needed. The instance will be created in a prisma.ts
file inside the lib/
directory. Go ahead and create the missing directory and file:
1mkdir lib && touch lib/prisma.ts
Now, add the following code to this file:
1// lib/prisma.ts2import { PrismaClient } from '@prisma/client';3
4let prisma: PrismaClient;5
6if (process.env.NODE_ENV === 'production') {7 prisma = new PrismaClient();8} else {9 if (!global.prisma) {10 global.prisma = new PrismaClient();11 }12 prisma = global.prisma;13}14
15export default prisma;
Now, whenever you need access to your database you can import the prisma
instance into the file where it's needed.
Step 5. Update the existing views to load data from the database
The blog post feed that's implemented in pages/index.tsx
and the post detail view in pages/p/[id].tsx
are currently returning hardcoded data. In this step, you'll adjust the implementation to return data from the database using Prisma Client.
Open pages/index.tsx
and add the following code right below the existing import
declarations:
1// pages/index.tsx2import prisma from '../lib/prisma';
Your prisma
instance will be your interface to the database when you want to read and write data in it. You can for example create a new User
record by calling prisma.user.create()
or retrieve all the Post
records from the database with prisma.post.findMany()
. For an overview of the full Prisma Client API, visit the Prisma docs.
Now you can replace the hardcoded feed
object in getStaticProps
inside index.tsx
with a proper call to the database:
1// index.tsx2export const getStaticProps: GetStaticProps = async () => {3 const feed = await prisma.post.findMany({4 where: { published: true },5 include: {6 author: {7 select: { name: true },8 },9 },10 });11 return {12 props: { feed },13 revalidate: 10,14 };15};
The two things to note about the Prisma Client query:
- A
where
filter is specified to include onlyPost
records wherepublished
istrue
- The
name
of theauthor
of thePost
record is queried as well and will be included in the returned objects
Before running the app, head over to /pages/p/[id].tsx
and adjust the implementation there as well to read the correct Post
record from the database.
This page uses getServerSideProps
(SSR) instead of getStaticProps
(SSG). This is because the data is dynamic, it depends on the id
of the Post
that's requested in the URL. For example, the view on route /p/42
displays the Post
where the id
is 42
.
Like before, you first need to import Prisma Client on the page:
1// pages/p/[id].tsx2import prisma from '../../lib/prisma';
Now you can update the implementation of getServerSideProps
to retrieve the proper post from the database and make it available to your frontend via the component's props
:
1// pages/p/[id].tsx2export const getServerSideProps: GetServerSideProps = async ({ params }) => {3 const post = await prisma.post.findUnique({4 where: {5 id: String(params?.id),6 },7 include: {8 author: {9 select: { name: true },10 },11 },12 });13 return {14 props: post,15 };16};
That's it! If your app is not running any more, you can restart it with the following command:
1npm run dev
Otherwise, save the files and open the app at http://localhost:3000
in your browser. The Post
record will be displayed as follows:

You can also click on the post to navigate to its detail view.
Step 6. Set up GitHub authentication with NextAuth
In this step, you will add GitHub authentication to the app. Once that functionality is available, you'll add more features to the app, such that authenticated users can create, publish and delete posts via the UI.
As a first step, go ahead and install the NextAuth.js library in your app:
1npm install next-auth@4 @next-auth/prisma-adapter
Next, you need to change your database schema to add the missing tables that are required by NextAuth.
To change your database schema, you can manually make changes to your Prisma schema and then run the prisma db push
command again. Open schema.prisma
and adjust the models in it to look as follows:
1// schema.prisma2
3model Post {4 id String @id @default(cuid())5 title String6 content String?7 published Boolean @default(false)8 author User?@relation(fields:[authorId], references:[id])9 authorId String?}10
11model Account {12 id String @id @default(cuid())13 userId String @map("user_id")14 type String15 provider String16 providerAccountId String @map("provider_account_id")17 refresh_token String?18 access_token String?19 expires_at Int?20 token_type String?21 scope String?22 id_token String?23 session_state String?24 oauth_token_secret String?25 oauth_token String?26
27 user User @relation(fields:[userId], references:[id], onDelete: Cascade)28
29 @@unique([provider, providerAccountId])}30
31model Session {32 id String @id @default(cuid())33 sessionToken String @unique@map("session_token")34 userId String @map("user_id")35 expires DateTime36 user User @relation(fields:[userId], references:[id], onDelete: Cascade)}37
38model User {39 id String @id @default(cuid())40 name String?41 email String?@unique42 emailVerified DateTime?43 image String?44 posts Post[]45 accounts Account[]46 sessions Session[]}47
48model VerificationToken {49 id Int @id @default(autoincrement())50 identifier String51 token String @unique52 expires DateTime53
54 @@unique([identifier, token])}55}
To learn more about these models, visit the NextAuth.js docs.
Now you can adjust your database schema by creating the actual tables in the database. Run the following command:
1npx prisma db push
Since you're using GitHub authentication, you also need to create a new OAuth app on GitHub. First, log into your GitHub account. Then, navigate to Settings, then open to Developer Settings, then switch to OAuth Apps.

Clicking on the Register a new application (or New OAuth App) button will redirect you to a registration form to fill out some information for your app. The Authorization callback URL should be the Next.js /api/auth
route: http://localhost:3000/api/auth
.
An important thing to note here is that the Authorization callback URL field only supports a single URL, unlike e.g. Auth0, which allows you to add additional callback URLs separated with a comma. This means if you want to deploy your app later with a production URL, you will need to set up a new GitHub OAuth app.

Click on the Register application button, and then you will be able to find your newly generated Client ID and Client Secret. Copy and paste this info into the .env
file in the root directory as the GITHUB_ID
and GITHUB_SECRET
env vars. Also set the NEXTAUTH_URL
to the same value of the Authorization callback URL thar you configured on GitHub: http://localhost:3000/api/auth
1# .env2
3# GitHub OAuth4GITHUB_ID=6bafeb321963449bdf515GITHUB_SECRET=509298c32faa283f28679ad6de6f86b2472e1bff6NEXTAUTH_URL=http://localhost:3000/api/auth
You will also need to persist a user's authentication state across the entire application. Make a quick change in your application's root file _app.tsx
and wrap your current root component with a SessionProvider
from the next-auth/react
package. Open the file and replace its current contents with the following code:
1// _app.tsx2
3import { SessionProvider } from 'next-auth/react';4import { AppProps } from 'next/app';5
6const App = ({ Component, pageProps }: AppProps) => {7 return (8 <SessionProvider session={pageProps.session}>9 <Component {...pageProps} />10 </SessionProvider>11 );12};13
14export default App;
Step 7. Add Log In functionality
The login button and some other UI components will be added to the Header.tsx
file. Open the file and paste the following code into it:
1// Header.tsx2import React from 'react';3import Link from 'next/link';4import { useRouter } from 'next/router';5import { signOut, useSession } from 'next-auth/react';6
7const Header: React.FC = () => {8 const router = useRouter();9 const isActive: (pathname: string) => boolean = (pathname) =>10 router.pathname === pathname;11
12 const { data: session, status } = useSession();13
14 let left = (15 <div className="left">16 <Link href="/">17 <a className="bold" data-active={isActive('/')}>18 Feed19 </a>20 </Link>21 <style jsx>{`22 .bold {23 font-weight: bold;24 }25
26 a {27 text-decoration: none;28 color: var(--geist-foreground);29 display: inline-block;30 }31
32 .left a[data-active='true'] {33 color: gray;34 }35
36 a + a {37 margin-left: 1rem;38 }39 `}</style>40 </div>41 );42
43 let right = null;44
45 if (status === 'loading') {46 left = (47 <div className="left">48 <Link href="/">49 <a className="bold" data-active={isActive('/')}>50 Feed51 </a>52 </Link>53 <style jsx>{`54 .bold {55 font-weight: bold;56 }57
58 a {59 text-decoration: none;60 color: var(--geist-foreground);61 display: inline-block;62 }63
64 .left a[data-active='true'] {65 color: gray;66 }67
68 a + a {69 margin-left: 1rem;70 }71 `}</style>72 </div>73 );74 right = (75 <div className="right">76 <p>Validating session ...</p>77 <style jsx>{`78 .right {79 margin-left: auto;80 }81 `}</style>82 </div>83 );84 }85
86 if (!session) {87 right = (88 <div className="right">89 <Link href="/api/auth/signin">90 <a data-active={isActive('/signup')}>Log in</a>91 </Link>92 <style jsx>{`93 a {94 text-decoration: none;95 color: var(--geist-foreground);96 display: inline-block;97 }98
99 a + a {100 margin-left: 1rem;101 }102
103 .right {104 margin-left: auto;105 }106
107 .right a {108 border: 1px solid var(--geist-foreground);109 padding: 0.5rem 1rem;110 border-radius: 3px;111 }112 `}</style>113 </div>114 );115 }116
117 if (session) {118 left = (119 <div className="left">120 <Link href="/">121 <a className="bold" data-active={isActive('/')}>122 Feed123 </a>124 </Link>125 <Link href="/drafts">126 <a data-active={isActive('/drafts')}>My drafts</a>127 </Link>128 <style jsx>{`129 .bold {130 font-weight: bold;131 }132
133 a {134 text-decoration: none;135 color: var(--geist-foreground);136 display: inline-block;137 }138
139 .left a[data-active='true'] {140 color: gray;141 }142
143 a + a {144 margin-left: 1rem;145 }146 `}</style>147 </div>148 );149 right = (150 <div className="right">151 <p>152 {session.user.name} ({session.user.email})153 </p>154 <Link href="/create">155 <button>156 <a>New post</a>157 </button>158 </Link>159 <button onClick={() => signOut()}>160 <a>Log out</a>161 </button>162 <style jsx>{`163 a {164 text-decoration: none;165 color: var(--geist-foreground);166 display: inline-block;167 }168
169 p {170 display: inline-block;171 font-size: 13px;172 padding-right: 1rem;173 }174
175 a + a {176 margin-left: 1rem;177 }178
179 .right {180 margin-left: auto;181 }182
183 .right a {184 border: 1px solid var(--geist-foreground);185 padding: 0.5rem 1rem;186 border-radius: 3px;187 }188
189 button {190 border: none;191 }192 `}</style>193 </div>194 );195 }196
197 return (198 <nav>199 {left}200 {right}201 <style jsx>{`202 nav {203 display: flex;204 padding: 2rem;205 align-items: center;206 }207 `}</style>208 </nav>209 );210};211
212export default Header;
Here's an overview of how the header is going to render:
- If no user is authenticated, a Log in button will be shown.
- If a user is authenticated, My drafts, New Post and Log out buttons will be shown.
You can already run the app to validate that this works by running npm run dev
, you'll find that the Log in button is now shown. However, if you click it, it does navigate you to http://localhost:3000/api/auth/signin
but Next.js is going to render a 404 page for you.
That's because NextAuth.js requires you to set up a specific route for authentication. You'll do that next.
Create a new directory and a new file in the pages/api
directory:
1mkdir -p pages/api/auth && touch pages/api/auth/[...nextauth].ts
In this new pages/api/auth/[...nextauth].ts
file, you now need to add the following boilerplate to configure your NextAuth.js setup with your GitHub OAuth credentials and the Prisma adapter:
1// pages/api/auth/[...nextauth].ts2
3import { NextApiHandler } from 'next';4import NextAuth from 'next-auth';5import { PrismaAdapter } from '@next-auth/prisma-adapter';6import GitHubProvider from 'next-auth/providers/github';7import prisma from '../../../lib/prisma';8
9const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);10export default authHandler;11
12const options = {13 providers: [14 GitHubProvider({15 clientId: process.env.GITHUB_ID,16 clientSecret: process.env.GITHUB_SECRET,17 }),18 ],19 adapter: PrismaAdapter(prisma),20 secret: process.env.SECRET,21};
Once the code is added, you can navigate to http://localhost:3000/api/auth/signin
again. This time, the Sign in with GitHub button is shown.

If you click it, you're forwarded to GitHub, where you can authenticate with your GitHub credentials. Once the authentication is done, you'll be redirected back into the app.
Note: If you're seeing an error and could not be authenticated, stop the app and re-run it with npm run dev
.
The header layout has now changed to display the buttons for authenticated users.

Step 8. Add new post functionality
In this step, you'll implement a way for a user to create a new post. The user can use this feature by clicking the New post button once they're authenticated.
The button already forwards to the /create
route, however, this currently leads to a 404 because that route is not implemented yet.
To fix that, create a new file in the pages directory that's called create.tsx
:
1touch pages/create.tsx
Now, add the following code to the newly created file:
1// pages/create.tsx2
3import React, { useState } from 'react';4import Layout from '../components/Layout';5import Router from 'next/router';6
7const Draft: React.FC = () => {8 const [title, setTitle] = useState('');9 const [content, setContent] = useState('');10
11 const submitData = async (e: React.SyntheticEvent) => {12 e.preventDefault();13 // TODO14 // You will implement this next ...15 };16
17 return (18 <Layout>19 <div>20 <form onSubmit={submitData}>21 <h1>New Draft</h1>22 <input23 autoFocus24 onChange={(e) => setTitle(e.target.value)}25 placeholder="Title"26 type="text"27 value={title}28 />29 <textarea30 cols={50}31 onChange={(e) => setContent(e.target.value)}32 placeholder="Content"33 rows={8}34 value={content}35 />36 <input disabled={!content || !title} type="submit" value="Create" />37 <a className="back" href="#" onClick={() => Router.push('/')}>38 or Cancel39 </a>40 </form>41 </div>42 <style jsx>{`43 .page {44 background: var(--geist-background);45 padding: 3rem;46 display: flex;47 justify-content: center;48 align-items: center;49 }50
51 input[type='text'],52 textarea {53 width: 100%;54 padding: 0.5rem;55 margin: 0.5rem 0;56 border-radius: 0.25rem;57 border: 0.125rem solid rgba(0, 0, 0, 0.2);58 }59
60 input[type='submit'] {61 background: #ececec;62 border: 0;63 padding: 1rem 2rem;64 }65
66 .back {67 margin-left: 1rem;68 }69 `}</style>70 </Layout>71 );72};73
74export default Draft;
This page is wrapped by the Layout
component so that it still includes the Header
and any other generic UI components.
It renders a form with several input fields. When submitted, the (right now empty) submitData
function is called. In that function, you need to pass the data from the React component to an API route which can then handle the actual storage of the new post data in the database.
Here's how you can implement the function:
1// /pages/create.tsx2
3const submitData = async (e: React.SyntheticEvent) => {4 e.preventDefault();5 try {6 const body = { title, content };7 await fetch('/api/post', {8 method: 'POST',9 headers: { 'Content-Type': 'application/json' },10 body: JSON.stringify(body),11 });12 await Router.push('/drafts');13 } catch (error) {14 console.error(error);15 }16};
In this code, you're using the title
and content
properties that are extracted from the component state using useState
and submit them via an HTTP POST request to the api/post
API route.
Afterwards, you're redirecting the user to the /drafts
page so that they can immediately see their newly created draft. If you run the app, the /create
route renders the following UI:

Note however that the implementation doesn't quite work yet because neither api/post
nor the /drafts
route exist so far. You'll implement these next.
First, let's make sure your backend can handle the POST request that's submitted by the user. Thanks to the Next.js API routes feature, you don't have to "leave your Next.js app" to implement such functionality but instead you can add it to your pages/api
directory.
Create a new directory called post
with a new file called index.ts
:
1mkdir -p pages/api/post && touch pages/api/post/index.ts
Note: At this point, you could also have created a file called pages/api/post.ts
` instead of taking the detour with an extra directory and an index.ts
file. The reason why you're not doing it that way is because you'll need to add a dynamic route for HTTP DELETE
requests at the api/post
route later as well. In order to save some refactoring later, you're already structuring the files in the required way.
Now, add the following code to pages/api/post/index.ts
:
1// pages/api/post/index.ts2
3import { getSession } from 'next-auth/react';4import prisma from '../../../lib/prisma';5
6// POST /api/post7// Required fields in body: title8// Optional fields in body: content9export default async function handle(req, res) {10 const { title, content } = req.body;11
12 const session = await getSession({ req });13 const result = await prisma.post.create({14 data: {15 title: title,16 content: content,17 author: { connect: { email: session?.user?.email } },18 },19 });20 res.json(result);21}
This code implements the handler function for any requests coming in at the /api/post/
route. The implementation does the following: First it extracts the title
and cotent
from the body of the incoming HTTP POST request. After that, it checks whether the request is coming from an authenticated user with the getSession
helper function from NextAuth.js. And finally, it uses Prisma Client to create a new Post
record in the database.
You can now test this functionality by opening the app, making sure you're authenticated and create a new post with title and content:

Once you click Create, the Post
record will be added to the database. Note that the /drafts
route that you're being redirected to right after the creation still renders a 404, that will be fixed soon. However, if you run Prisma Studio again with npx prisma studio
, you'll see that the new Post
record has been added to the database.
Step 9. Add drafts functionality
In this step, you'll add a new page to the app that allows an authenticated user to view their current drafts.
This page can't be statically rendered because it depends on a user who is authenticated. Pages like this that get their data dynamically based on an authenticated users are a great use case for server-side rendering (SSR) via getServerSideProps
.
First, create a new file in the pages
directory and call it drafts.tsx
:
1touch pages/drafts.tsx
Next, add the following code to that file:
1// pages/drafts.tsx2
3import React from 'react';4import { GetServerSideProps } from 'next';5import { useSession, getSession } from 'next-auth/react';6import Layout from '../components/Layout';7import Post, { PostProps } from '../components/Post';8import prisma from '../lib/prisma';9
10export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {11 const session = await getSession({ req });12 if (!session) {13 res.statusCode = 403;14 return { props: { drafts: [] } };15 }16
17 const drafts = await prisma.post.findMany({18 where: {19 author: { email: session.user.email },20 published: false,21 },22 include: {23 author: {24 select: { name: true },25 },26 },27 });28 return {29 props: { drafts },30 };31};32
33type Props = {34 drafts: PostProps[];35};36
37const Drafts: React.FC<Props> = (props) => {38 const { data: session } = useSession();39
40 if (!session) {41 return (42 <Layout>43 <h1>My Drafts</h1>44 <div>You need to be authenticated to view this page.</div>45 </Layout>46 );47 }48
49 return (50 <Layout>51 <div className="page">52 <h1>My Drafts</h1>53 <main>54 {props.drafts.map((post) => (55 <div key={post.id} className="post">56 <Post post={post} />57 </div>58 ))}59 </main>60 </div>61 <style jsx>{`62 .post {63 background: var(--geist-background);64 transition: box-shadow 0.1s ease-in;65 }66
67 .post:hover {68 box-shadow: 1px 1px 3px #aaa;69 }70
71 .post + .post {72 margin-top: 2rem;73 }74 `}</style>75 </Layout>76 );77};78
79export default Drafts;
In this React component, you're rendering a list of "drafts" of the authenticated user. The drafts are retrieved from the database during server-side rendering, because the database query with Prisma Client is executed in getServerSideProps
. The data is then made available to the React component via its props
.
If you now navigate to the My drafts section of the app, you'll see the unpublished post that you created before:

Step 10. Add Publish functionality
To "move" the draft to the public feed view, you need to be able to "publish" it – that is, setting the published
field of a Post
record to true
. This functionality will be implemented in the post detail view that currently lives in pages/p/[id].tsx
.
The functionality will be implemented via an HTTP PUT request that'll be sent to a api/publish
route in your "Next.js backend". Go ahead and implement that route first.
Create a new directory inside the pages/api
directory called publish
. Then create a new file called [id].ts
in the new directory:
1mkdir -p pages/api/publish && touch pages/api/publish/[id].ts
Now, add the following code to the newly created file:
1// pages/api/publish/[id].ts2
3import prisma from '../../../lib/prisma';4
5// PUT /api/publish/:id6export default async function handle(req, res) {7 const postId = req.query.id;8 const post = await prisma.post.update({9 where: { id: postId },10 data: { published: true },11 });12 res.json(post);13}
This is the implementation of an API route handler which retrieves the ID of a Post
from the URL and then uses Prisma Client's update
method to set the published
field of the Post
record to true
.
Next, you'll implement the functionality on the frontend in the pages/p/[id].tsx
file. Open up the file and replace its contents with the following:
1// pages/p/[id].tsx2
3import React from 'react';4import { GetServerSideProps } from 'next';5import ReactMarkdown from 'react-markdown';6import Router from 'next/router';7import Layout from '../../components/Layout';8import { PostProps } from '../../components/Post';9import { useSession } from 'next-auth/react';10import prisma from '../../lib/prisma';11
12export const getServerSideProps: GetServerSideProps = async ({ params }) => {13 const post = await prisma.post.findUnique({14 where: {15 id: String(params?.id),16 },17 include: {18 author: {19 select: { name: true, email: true },20 },21 },22 });23 return {24 props: post,25 };26};27
28async function publishPost(id: string): Promise<void> {29 await fetch(`/api/publish/${id}`, {30 method: 'PUT',31 });32 await Router.push('/');33}34
35const Post: React.FC<PostProps> = (props) => {36 const { data: session, status } = useSession();37 if (status === 'loading') {38 return <div>Authenticating ...</div>;39 }40 const userHasValidSession = Boolean(session);41 const postBelongsToUser = session?.user?.email === props.author?.email;42 let title = props.title;43 if (!props.published) {44 title = `${title} (Draft)`;45 }46
47 return (48 <Layout>49 <div>50 <h2>{title}</h2>51 <p>By {props?.author?.name || 'Unknown author'}</p>52 <ReactMarkdown children={props.content} />53 {!props.published && userHasValidSession && postBelongsToUser && (54 <button onClick={() => publishPost(props.id)}>Publish</button>55 )}56 </div>57 <style jsx>{`58 .page {59 background: var(--geist-background);60 padding: 2rem;61 }62
63 .actions {64 margin-top: 2rem;65 }66
67 button {68 background: #ececec;69 border: 0;70 border-radius: 0.125rem;71 padding: 1rem 2rem;72 }73
74 button + button {75 margin-left: 1rem;76 }77 `}</style>78 </Layout>79 );80};81
82export default Post;
This code adds the publishPost
function to the React component which is responsible for sending the HTTP PUT request to the API route you just implemented. The render
function of the component is also adjusted to check whether the user is authenticated, and if that's the case, it'll display the Publish button in the post detail view as well:

If you click the button, you will be redirected to the public feed and the post will be displayed there!
Note: Once the app is deployed to production, the feed will be updated at most every 10 seconds when it receives a request. That's because you're using static site generation (SSG) via getStaticProps
to retrieve the data for this view with Incremental Static Regeneration. If you want data to be updated "immediately", consider using On-Demand Incremental Static Regeneration.
Step 11. Add Delete functionality
The last piece of functionality you'll implement in this guide is to enable users to delete existing Post
records. You'll follow a similar approach as for the "publish" functionality by first implementing the API route handler on the backend, and then adjust your frontend to make use of the new route!
Create a new file in the pages/api/post
directory and call it [id].ts
:
1touch pages/api/post/[id].ts
Now, add the following code to it:
1// pages/api/post/[id].ts2
3import prisma from '../../../lib/prisma';4
5// DELETE /api/post/:id6export default async function handle(req, res) {7 const postId = req.query.id;8 if (req.method === 'DELETE') {9 const post = await prisma.post.delete({10 where: { id: postId },11 });12 res.json(post);13 } else {14 throw new Error(15 `The HTTP ${req.method} method is not supported at this route.`,16 );17 }18}
This code handles HTTP DELETE
requests that are coming in via the /api/post/:id
URL. The route handler then retrieves the id
of the Post
record from the URL and uses Prisma Client to delete this record in the database.
To make use of this feature on the frontend, you again need to adjust the post detail view. Open pages/p/[id].tsx
and insert the following function right below the publishPost
function:
1// pages/p/[id].tsx2
3async function deletePost(id: string): Promise<void> {4 await fetch(`/api/post/${id}`, {5 method: 'DELETE',6 });7 Router.push('/');8}
Now, you can follow a similar approach with the Delete button as you did with the Publish button and render it only if the user is authenticated. To achieve this, you can add this code directly in the return
part of the Post
component right below where the Publish button is rendered:
1// pages/p/[id].tsx2{3 !props.published && userHasValidSession && postBelongsToUser && (4 <button onClick={() => publishPost(props.id)}>Publish</button>5 );6}7{8 userHasValidSession && postBelongsToUser && (9 <button onClick={() => deletePost(props.id)}>Delete</button>10 );11}
You can now try out the new functionality by creating a new draft, navigating to its detail view and then clicking the newly appearing Delete button:

Step 12. Deploy to Vercel
In this final step, you're going to deploy the app to Vercel from a GitHub repo.
Before you can deploy, you need to:
- Create another OAuth app on GitHub
- Create a new GitHub repo and push your project to it
To start with the OAuth app, go back to step "Step 5. Set up GitHub authentication with NextAuth" and follow the steps to create another OAuth app via the GitHub UI.
This time, the Authorization Callback URL needs to match the domain of your future Vercel deployment which will be based on the Vercel project name. As a Vercel project name, you will choose blogr-nextjs-prisma
prepended with your first and lastname: FIRSTNAME-LASTNAME-blogr-nextjs-prisma
. For example, if you're called "Jane Doe", your project name should be jane-doe-blogr-nextjs-prisma
.
Note: Prepending your first and last name is required to ensure the uniqueness of your deployment URL.
The Authorization Callback URL must therefore be set to https://FIRSTNAME-LASTNAME-blogr-nextjs-prisma.vercel.app/api/auth
. Once you created the application, adjust your .env
file and set the Client ID as the GITHUB_ID
env var and a Client secret as the GITHUB_SECRET
env var. The NEXTAUTH_URL
env var needs to be set to the same value as the Authorization Callback URL on GitHub: https://FIRSTNAME-LASTNAME-blogr-nextjs-prisma.vercel.app/api/auth
.

Next, create a new GitHub repository with the same name, e.g. jane-doe-blogr-nextjs-prisma
. Now, copy the three terminal commands from the bottom section that says ...or push an existing repository from the command line, it should look similar to this:
1git remote add origin git@github.com:janedoe/jane-doe-blogr-nextjs-prisma.git2git branch -M main3git push -u origin main
You now should have your new repository ready at https://github.com/GITHUB_USERNAME/FIRSTNAME-LASTNAME-blogr-nextjs-prisma
, e.g. https://github.com/janedoe/jane-doe-blogr-nextjs-prisma
.
With the GitHub repo in place, you can now import it to Vercel in order to deploy the app:
Now, provide the URL of your GitHub repo in the text field:

Click Continue. The next screen requires you to set the environment variables for your production deployment:

Here's what you need to provide:
GITHUB_ID
: Set this to the Client ID of the GitHub OAuth app you just createdGITHUB_SECRET
: Set this to the Client Secret of the GitHub OAuth app you just createdNEXTAUTH_URL
: Set this to the Authorization Callback URL of the GitHub OAuth app you just createdSECRET
: Set this to your own strong secret. This was not needed in development as NextAuth.js will generate one if not provided. However, you will need to provide your own value for production otherwise you will receive an error.
You'll also need to link your Vercel postgres database to this Vercel project so that all your database environment variables are automatically added. Once all environment variables are set, hit Deploy. Your app is now being deployed to Vercel. Once it's ready, Vercel will show you the following success screen:

You can click the Visit button to view the deployed version of your fullstack app 🎉
Conclusion
In this guide, you learned how to build and deploy a fullstack application using Next.js, Prisma, and Vercel Postgres. If you ran into issue or have any questions about this guide, feel free to raise them on GitHub or drop them in the Prisma Slack.