Let's Build a Basic React App
React, Next.js, Next.js API Routes, Prisma, Vercel Postgres, NextAuth.js, TypeScript, Vercel, Git and GitHub
This article is going to cover the basics of creating a React web application from beginning to end, including:
Next.js as the React framework
Next.js API Routes for server-side API routes
Prisma as the ORM for database access
Vercel Postgres as the database
NextAuth.js for authentication
TypeScript as the programming language
Vercel for hosting
Git and GitHub for change management pushing changes from your machine to the web.
Much of this structure is pulled from https://vercel.com/guides/nextjs-prisma-postgres with some additional steps and some bug fixes that will get you up and running faster.
Create App and Production Pipeline
Creating a React App Locally
npx create-next-app --example https://github.com/prisma/blogr-nextjs-prisma/tree/main blogr-nextjs-prisma
If you do not yet have Node yet on your machine, you can download it at https://nodejs.org/en/download
Now you can run the React app locally with
cd blogr-nextjs-prisma
npm run dev
And visit http://localhost:3000
Get Git Configured
In GitHub, create a new repo called blogr-nextjs-prisma.
Within the working directory (blogr-nextjs-prisma) run these commands
git init
git add .
git commit -m "initial commit"
# replace xxx with your github username
git remote add origin https://github.com/xxx/blogr-nextjs-prisma.git
# you cannot use your password - need to use a generated access token https://github.com/settings/tokens
git push origin main
Connect GitHub and Vercel
Head over to https://vercel.com/new and import your newly created repo (blogr-nextjs-prisma) from GitHub.
This will wire it up so that when you push changes up to your GitHub repo, your app will automatically get built and deployed to Vercel.
Bring it Into Visual Studio Code
Open up VS Code, File > Open Folder and open up the blogr-nextjs-prisma directory. Go ahead and make a simple change (like adding a ! after the author's name on the Post.tsx file). Add your message and click Commit & Push.
You can verify that the build is in progress at https://vercel.com/ > blogr-nextjs-prisma > Deployments and then click on the deployment id (the 9 character id above the word Production)
Once it gets to Ready status, you can click the Visit button and verify that the site has been updated.
Power Up the PostgreSQL database
Set Up Database
In your Vercel project, select the Storage > Connect Database. Under the Create New tab, select Postgres and then the Continue button.
This causes some environment variables to be created, and we need to pull these down to our local environment. To do that, you need to have the Vercel CLI, which can be installed with the first line. The second line pulls the remote file down.
sudo npm i -g vercel@latest
vercel env pull .env
At this point, your local React app is pointing to the cloud-based Vercel database (both your local app and your remote app will point to the same remote database)
Fire Up Prisma
npm install prisma --save-dev
Create a folder called prisma
and add a file called schema.prisma
and paste the following code into that file:
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
}
model Post {
id String @default(cuid()) @id
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId String?
}
model User {
id String @default(cuid()) @id
name String?
email String? @unique
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
posts Post[]
@@map(name: "users")
}
And then create the database and tables in Postgres using this command:
npx prisma db push
npm install @prisma/client
mkdir lib && touch lib/prisma.ts
npx prisma studio
This last line will open a web interface to your database. Make sure you add one post and one user (and relate them) so that you can see this in action.
You need to add this code to the prisma.ts
file you just created.
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.prisma) {
global.prisma = new PrismaClient();
}
prisma = global.prisma;
}
export default prisma;
Now, whenever you need access to your database you can import the prisma
instance into the file where it's needed.
Every time that you make a change to your Prisma schema, you need to update the Prisma Client with:
npx prisma generate
Now for Real Data
Update the Views
import prisma from '../lib/prisma';
Replace the getStaticProps within the pages/index.tsx file with the following code:
export const getStaticProps: GetStaticProps = async () => {
const feed = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { name: true },
},
},
});
return {
props: { feed },
revalidate: 10,
};
};
You can learn more about Prisma at Prisma docs
Update the Details View
Edit /pages/p/[id]
to include:
import prisma from '../../lib/prisma';
Now replace the getServerSideProps with the following:
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const post = await prisma.post.findUnique({
where: {
id: String(params?.id),
},
include: {
author: {
select: { name: true },
},
},
});
return {
props: post,
};
};
Test it out - see the beauty you have created.
npm run dev
# i had to update my react and react-dom in package.json
# and then run npm i -force
# error was 'Error: Next.js requires react >= 18.2.0 to be installed.'
To get this working in Vercel, I had to delete a couple of locked files, add "postinstall": "prisma generate"
to the package.json > scripts section, and updated Typescript to npm install -D typescript@latest - not sure which of that combination worked.
Auth On
NextAuth Readiness
Install Next Auth
npm install next-auth@4 @next-auth/prisma-adapter
Update schema.prisma
as required by NextAuth:
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
}
// schema.prisma
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User?@relation(fields:[authorId], references:[id])
authorId String?
}
model Account {
id String @id @default(cuid())
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
oauth_token_secret String?
oauth_token String?
user User @relation(fields:[userId], references:[id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique@map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields:[userId], references:[id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String?@unique
emailVerified DateTime?
image String?
posts Post[]
accounts Account[]
sessions Session[]
}
model VerificationToken {
id Int @id @default(autoincrement())
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
To get these changes into your database, run
npx prisma db push
Get GitHub App Configured
As described on OAuth app on GitHub, create a new GitHub OAuth app at https://github.com/settings/applications/new - using http://localhost:3000/api/auth
as the Authorization callback URL (you will create another GitHub OAuth app later for production).
Update Your .env
On the newly created GitHub OAuth app, click on "Generate a new client secret". This will give you a Client Id and Client Secrets. Copy them into your .env file in your root directory (GITHUB_ID=Client Id & GITHUB_SECRET=Client Secrets).
# .env
# GitHub OAuth
GITHUB_ID=6bafeb321963449bdf51
GITHUB_SECRET=509298c32faa283f28679ad6de6f86b2472e1bff
NEXTAUTH_URL=http://localhost:3000/api/auth
Make Session Persistent Across App
In order to persist a user's authentication state across the entire application, wrap the root component with SessionProvider
- which needs to be imported - as shown in _app.tsx
:
import { SessionProvider } from 'next-auth/react';
import { AppProps } from 'next/app';
const App = ({ Component, pageProps }: AppProps) => {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
);
};
export default App;
changed to this page https://authjs.dev/getting-started/providers/oauth-tutorial
Add Log In Functionality
Update Header.tsx
with the following:
import React from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { signOut, useSession } from 'next-auth/react';
const Header: React.FC = () => {
const router = useRouter();
const isActive: (pathname: string) => boolean = (pathname) =>
router.pathname === pathname;
const { data: session, status } = useSession();
let left = (
<div className="left">
<Link legacyBehavior href="/">
<a className="bold" data-active={isActive('/')}>
Feed
</a>
</Link>
<style jsx>{`
.bold {
font-weight: bold;
}
a {
text-decoration: none;
color: var(--geist-foreground);
display: inline-block;
}
.left a[data-active='true'] {
color: gray;
}
a + a {
margin-left: 1rem;
}
`}</style>
</div>
);
let right = null;
if (status === 'loading') {
left = (
<div className="left">
<Link legacyBehavior href="/">
<a className="bold" data-active={isActive('/')}>
Feed
</a>
</Link>
<style jsx>{`
.bold {
font-weight: bold;
}
a {
text-decoration: none;
color: var(--geist-foreground);
display: inline-block;
}
.left a[data-active='true'] {
color: gray;
}
a + a {
margin-left: 1rem;
}
`}</style>
</div>
);
right = (
<div className="right">
<p>Validating session ...</p>
<style jsx>{`
.right {
margin-left: auto;
}
`}</style>
</div>
);
}
if (!session) {
right = (
<div className="right">
<Link legacyBehavior href="/api/auth/signin">
<a data-active={isActive('/signup')}>Log in</a>
</Link>
<style jsx>{`
a {
text-decoration: none;
color: var(--geist-foreground);
display: inline-block;
}
a + a {
margin-left: 1rem;
}
.right {
margin-left: auto;
}
.right a {
border: 1px solid var(--geist-foreground);
padding: 0.5rem 1rem;
border-radius: 3px;
}
`}</style>
</div>
);
}
if (session) {
left = (
<div className="left">
<Link legacyBehavior href="/">
<a className="bold" data-active={isActive('/')}>
Feed
</a>
</Link>
<Link legacyBehavior href="/drafts">
<a data-active={isActive('/drafts')}>My drafts</a>
</Link>
<style jsx>{`
.bold {
font-weight: bold;
}
a {
text-decoration: none;
color: var(--geist-foreground);
display: inline-block;
}
.left a[data-active='true'] {
color: gray;
}
a + a {
margin-left: 1rem;
}
`}</style>
</div>
);
right = (
<div className="right">
<p>
{session.user.name} ({session.user.email})
</p>
<Link legacyBehavior href="/create">
<button>
<a>New post</a>
</button>
</Link>
<button onClick={() => signOut()}>
<a>Log out</a>
</button>
<style jsx>{`
a {
text-decoration: none;
color: var(--geist-foreground);
display: inline-block;
}
p {
display: inline-block;
font-size: 13px;
padding-right: 1rem;
}
a + a {
margin-left: 1rem;
}
.right {
margin-left: auto;
}
.right a {
border: 1px solid var(--geist-foreground);
padding: 0.5rem 1rem;
border-radius: 3px;
}
button {
border: none;
}
`}</style>
</div>
);
}
return (
<nav>
{left}
{right}
<style jsx>{`
nav {
display: flex;
padding: 2rem;
align-items: center;
}
`}</style>
</nav>
);
};
export default Header;
Note that each Link has a legacyBehavior
attribute added to it.
To check it out go
npm run dev
Create Login Page
mkdir -p pages/api/auth && touch pages/api/auth/[...nextauth].ts
Then add this to the newly created [...nextauth].ts file
import { NextApiHandler } from 'next';
import NextAuth from 'next-auth';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import GitHubProvider from 'next-auth/providers/github';
import prisma from '../../../lib/prisma';
const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;
const options = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET,
};
Create Content
Add a Post to Your Blog
Create the create page
touch pages/create.tsx
And then update the create page
import React, { useState } from 'react';
import Layout from '../components/Layout';
import Router from 'next/router';
const Draft: React.FC = () => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const submitData = async (e: React.SyntheticEvent) => {
e.preventDefault();
try {
const body = { title, content };
await fetch('/api/post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
await Router.push('/drafts');
} catch (error) {
console.error(error);
}
};
return (
<Layout>
<div>
<form onSubmit={submitData}>
<h1>New Draft</h1>
<input
autoFocus
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
type="text"
value={title}
/>
<textarea
cols={50}
onChange={(e) => setContent(e.target.value)}
placeholder="Content"
rows={8}
value={content}
/>
<input disabled={!content || !title} type="submit" value="Create" />
<a className="back" href="#" onClick={() => Router.push('/')}>
or Cancel
</a>
</form>
</div>
<style jsx>{`
.page {
background: var(--geist-background);
padding: 3rem;
display: flex;
justify-content: center;
align-items: center;
}
input[type='text'],
textarea {
width: 100%;
padding: 0.5rem;
margin: 0.5rem 0;
border-radius: 0.25rem;
border: 0.125rem solid rgba(0, 0, 0, 0.2);
}
input[type='submit'] {
background: #ececec;
border: 0;
padding: 1rem 2rem;
}
.back {
margin-left: 1rem;
}
`}</style>
</Layout>
);
};
export default Draft;
Now we need to create a post route for handling the submission of the form.
mkdir -p pages/api/post && touch pages/api/post/index.ts
And here is the logic for making the magic of post happen.
import { getSession } from 'next-auth/react';
import prisma from '../../../lib/prisma';
// POST /api/post
// Required fields in body: title
// Optional fields in body: content
export default async function handle(req, res) {
const { title, content } = req.body;
const session = await getSession({ req });
const result = await prisma.post.create({
data: {
title: title,
content: content,
author: { connect: { email: session?.user?.email } },
},
});
res.json(result);
}
At this point, the drafts should created, but there is no drafts page. Time to go exploring!!
Debugging with Visual Studio Code
Click on the Run & Debug icon
You will get a requirement to create a launch.json
file - do it. You also may need to install the .net SDK so that you can run your node instance locally https://dotnet.microsoft.com/en-us/download/dotnet/sdk-for-vs-code
The first problem I saw when debugging is this:
[next-auth][warn][NO_SECRET]
https://next-auth.js.org/warnings#no_secret
Not the error I was looking to resolve, but worth addressing because this will cause a problem in production.
$ openssl rand -base64 32
Then paste that into your .env
file as a new value for NEXTAUTH_SECRET
Bugs in the Tutorial
I spent a fair bit of time spinning my wheels on this. I was getting an error "argument connect
of type userwhereuniqueinput needs at least one of id
or email
arguments." It looked like the session variable was not getting set. const session = await getSession({ req })
. I started poking around, and finally found some clues on the Prisma Slack page that pointed me to the most beautiful discussion https://github.com/prisma/prisma/discussions/19400 - thank you Daniel Ughelli for figuring this out!
You need to update your ...nextauth like this:
// pages/api/auth/[...nextauth].ts
import { NextApiHandler } from "next";
import NextAuth from "next-auth";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import GitHubProvider from "next-auth/providers/github";
import prisma from "../../../lib/prisma";
export const options = { // <--- exported the nextauth options
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET,
};
const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;
And your post API page like this:
// pages/api/post/index.ts
// import { getSession } from 'next-auth/react'; // <--- removed getSession import
import { getServerSession } from "next-auth/next" // <--- imported getServerSession from "next-auth/next"
import { options as authOptions } from "../auth/[...nextauth]" // <--- imported authOptions
import prisma from "../../../lib/prisma";
// POST /api/post
// Required fields in body: title
// Optional fields in body: content
export default async function handle(req, res) {
const { title, content } = req.body;
// const session = await getSession({ req }); // <--- removed getSession call
const session = await getServerSession(req, res, authOptions); // <--- used the getServerSession instead
const result = await prisma.post.create({
data: {
title: title,
content: content,
author: { connect: { email: session?.user?.email } },
},
});
res.json(result);
}
Once I made these changes, my session was populated, and I immediately started posting drafts. Would be great if Prisma would update its walkthrough.
Creating the Drafts Page
touch pages/drafts.tsx
Now update the drafts.tsx page you just created with:
import React from 'react';
import { GetServerSideProps } from 'next';
import { useSession, getSession } from 'next-auth/react';
import Layout from '../components/Layout';
import Post, { PostProps } from '../components/Post';
import prisma from '../lib/prisma';
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const session = await getSession({ req });
if (!session) {
res.statusCode = 403;
return { props: { drafts: [] } };
}
const drafts = await prisma.post.findMany({
where: {
author: { email: session.user.email },
published: false,
},
include: {
author: {
select: { name: true },
},
},
});
return {
props: { drafts },
};
};
type Props = {
drafts: PostProps[];
};
const Drafts: React.FC<Props> = (props) => {
const { data: session } = useSession();
if (!session) {
return (
<Layout>
<h1>My Drafts</h1>
<div>You need to be authenticated to view this page.</div>
</Layout>
);
}
return (
<Layout>
<div className="page">
<h1>My Drafts</h1>
<main>
{props.drafts.map((post) => (
<div key={post.id} className="post">
<Post post={post} />
</div>
))}
</main>
</div>
<style jsx>{`
.post {
background: var(--geist-background);
transition: box-shadow 0.1s ease-in;
}
.post:hover {
box-shadow: 1px 1px 3px #aaa;
}
.post + .post {
margin-top: 2rem;
}
`}</style>
</Layout>
);
};
export default Drafts;
Push to Vercel
We are going to deviate from the tutorial here and just get this working on production.
Commit & push your changes, then create another GitHub OAuth app at https://github.com/settings/applications/new
Add your environment variables to your project on Vercel (under Settings)
Remember to include your secrets.
Remember that you need to redeploy after you add any environment variables to get them picked up.
Conclusion
You now have a pretty basic web app with authentication via NextAuth, hosting with Vercel, software pipelines with GitHub, ORM with Prisma, and data management with Vercel Postgres. Here is a link to the website frozen in time at this point of the development cycle https://blogr-nextjs-prisma-20atrk34i-dyor1.vercel.app/ and here is a link to the github repo https://github.com/dyor/blogr-nextjs-prisma/commit/a31a53ac8d196c0058519067641a6b48a45b2ec8
For my next exploration, I may either go deeper with this single app template, or I may dig in to Vercel's Platform starter kit https://vercel.com/new/templates/next.js/platforms-starter-kit that allows a builder to support multiple customers with multiple domains under one project. Or I may explore a totally different stack if I get a good recommendation.