Part 7: Adding Simple Authentication to blog-demo (Next.js + Prisma + Auth.js)

In the previous article, we already got blog-demo running in Next.js: we can list articles, create articles, view details, and add comments.
But there is an obvious problem: all write operations use the hardcoded authorId = 1 by default, with no login concept at all.
In this article, we will do three things:
Add a minimal usable login system (Auth.js / NextAuth style) to the project;
Change blog-demo’s “New Article / Post a Comment” to “Must be logged in to operate”;
In the API Route, retrieve the current user ID from the session instead of the hardcoded authorId.
We won’t use third-party OAuth, nor a complex registration process. We’ll only use the simplest version of Email/Password or Credentials login to build the overall skeleton of “Prisma + Next.js + Auth”.
Note: This article focuses on “engineering connection”, not on the full functionality of Auth.js, so the configuration will be as concise and clear as possible.
1. Choosing an Auth Solution: Why Auth.js / NextAuth
Next.js’s recommended auth solution is Auth.js (formerly NextAuth). Community examples almost always pair it with Prisma.
The benefits are:
It has a dedicated Prisma Adapter, allowing user tables and session tables to be persisted directly in the database;
It works well with the App Router, with examples in the official docs;
We don’t need to design all security details ourselves.
In this article, we’ll use Auth.js as an example, focusing on:
How to integrate the Prisma User table with Auth.js;
How to get the current user in API Routes / Server Components.
2. Extending the Schema: Adding User Table Structure for Auth.js
Auth.js generally requires a set of models (User / Account / Session / VerificationToken). The official command can add these directly to the Prisma schema.
But we already have our own blog-demo User model and don’t want to be completely overwritten. There are two approaches:
Let Auth.js fully take over the user model: let it add its own
User, then reference thisUserin blog-demo’s relations;Let Auth.js use our existing
User, but add the necessary fields.
For simplicity, we will choose: unify the User model and let Auth.js also use it.
You can extend the existing User model slightly, e.g., add fields for login (like username or hashed password) and possibly a role field (role control will be used later).
model User {
id Int @id @default(autoincrement())
email String @unique
name String
// For simplicity, use username + hashedPassword
username String? @unique
password String? // Store encrypted password (must be hashed in production)
role String @default("USER") // Simple role
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
profile Profile?
posts Post[]
comments Comment[]
// If you use Auth.js PrismaAdapter later, you also need to add Account/Session etc. models per its docs
}If you plan to use the full Auth.js PrismaAdapter, it's best to follow the official guide to have it create all models at once, then adjust blog-demo’s relations to use its User.
To keep focus, we assume:
You already have a User that can log in (whether created by Auth.js or pre‑inserted);
We will focus on the part “get current user + restrict write operations”.
3. Configuring Auth.js in Next.js: auth.ts + /api/auth
Following the recommended usage for the App Router, we typically configure Auth.js in lib/auth.ts and export the handler in app/api/auth/[...nextauth]/route.ts.
3.1 Install Dependencies
In the project root:
pnpm add next-auth @auth/prisma-adapter3.2 Configure Auth.js: lib/auth.ts
Example lib/auth.ts (simplified, using Credentials Provider):
// lib/auth.ts
import NextAuth, { type NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { blogPrisma } from './prisma-blog'
import type { User as PrismaUser } from '../generated/blog'
export const authConfig: NextAuthConfig = {
adapter: PrismaAdapter(blogPrisma as any),
session: {
strategy: 'jwt',
},
providers: [
Credentials({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials.password) return null
const user = await blogPrisma.user.findUnique({
where: { email: credentials.email },
})
if (!user) return null
// Password verification should be done here (BCrypt etc.)
// For simplicity, allow any password for now
// if (!compareHash(credentials.password, user.password)) return null
return {
id: String(user.id),
name: user.name,
email: user.email,
role: user.role,
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = (user as any).role
}
return token
},
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string
;(session.user as any).role = token.role
}
return session
},
},
}
export const { handlers, auth, signIn, signOut } = NextAuth(authConfig)A few key points:
Using the
CredentialsProvider: only shows an “Email + Password” form;In
authorize, use Prisma to read the User table;In callbacks, write
user.idandroleinto the JWT, then sync them onto session.user;The
authfunction can be called in Server Components/APIs to get the session for the current request.
In production, proper password hashing verification should be used. Here we omit the specific password logic to focus on the overall structure of Prisma combined with auth.
3.3 /api/auth/[...nextauth] Route
app/api/auth/[...nextauth]/route.ts:
import { handlers } from '@/lib/auth'
// Auth.js will use GET/POST handlers
export const { GET, POST } = handlersAt this point, the Auth.js foundations are set. You can use the hooks from next-auth/react on the client to login/logout; on the server, use auth() to get current user info.
4. Using auth() in API Routes: Restrict Write Operations to Logged‑in Users
Now, let’s revisit the API Routes we wrote in Part 6, such as /api/posts and /api/posts/[id]/comments.
The goals are:
Remove the
authorIdsent in the body;Instead, read
userIdfrom the session;If not logged in, return 401 immediately.
4.1 Modify POST /api/posts: Use the Currently Logged‑in User as Author
Modify POST in app/api/posts/route.ts:
import { NextRequest, NextResponse } from 'next/server'
import { blogPrisma } from '@/lib/prisma-blog'
import { auth } from '@/lib/auth'
export async function POST(req: NextRequest) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Not logged in, cannot create a post' },
{ status: 401 },
)
}
const userId = Number(session.user.id)
const body = await req.json()
const { title, content, categorySlugs } = body
if (!title) {
return NextResponse.json(
{ error: 'title is required' },
{ status: 400 },
)
}
const post = await blogPrisma.$transaction(async (tx) => {
const created = await tx.post.create({
data: {
title,
content,
published: true,
authorId: userId,
},
})
if (Array.isArray(categorySlugs) && categorySlugs.length > 0) {
const categories = await tx.category.findMany({
where: { slug: { in: categorySlugs } },
})
if (categories.length > 0) {
await tx.postCategory.createMany({
data: categories.map((c) => ({
postId: created.id,
categoryId: c.id,
})),
skipDuplicates: true,
})
}
}
return await tx.post.findUnique({
where: { id: created.id },
include: {
author: { select: { id: true, name: true } },
categories: { include: { category: true } },
},
})
})
if (!post) {
return NextResponse.json({ error: 'Failed to create post' }, { status: 500 })
}
const result = {
id: post.id,
title: post.title,
content: post.content,
createdAt: post.createdAt,
author: post.author,
categories: post.categories.map((pc) => pc.category),
}
return NextResponse.json(result, { status: 201 })
} catch (e: any) {
console.error(e)
return NextResponse.json(
{ error: e.message || 'Failed to create post' },
{ status: 500 },
)
}
}Key changes:
Use
auth()to get the session, return 401 if absent;No longer allow the client to send
authorIdto prevent spoofing;All write operations are tied to the currently logged‑in user.
4.2 Modify POST /api/posts/[id]/comments: Use the Currently Logged‑in User as Commenter
Similarly, add login protection to the comment endpoint:
app/api/posts/[id]/comments/route.ts:
import { NextRequest, NextResponse } from 'next/server'
import { blogPrisma } from '@/lib/prisma-blog'
import { auth } from '@/lib/auth'
type RouteParams = {
params: { id: string }
}
export async function POST(req: NextRequest, { params }: RouteParams) {
try {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Not logged in, cannot comment' },
{ status: 401 },
)
}
const authorId = Number(session.user.id)
const postId = Number(params.id)
const body = await req.json()
const { content } = body
if (!content || !Number.isFinite(postId)) {
return NextResponse.json(
{ error: 'content and a valid postId are required' },
{ status: 400 },
)
}
const comment = await blogPrisma.$transaction(async (tx) => {
const post = await tx.post.findUnique({
where: { id: postId },
select: { id: true },
})
if (!post) throw new Error('Post does not exist')
return await tx.comment.create({
data: {
content,
postId,
authorId,
},
include: {
author: { select: { id: true, name: true } },
},
})
})
return NextResponse.json(comment, { status: 201 })
} catch (e: any) {
console.error(e)
const message =
e.message === 'Post does not exist' ? e.message : 'Failed to create comment'
const status = message === 'Post does not exist' ? 400 : 500
return NextResponse.json({ error: message }, { status })
}
}At this point, blog-demo’s “write” endpoints are tied to login status:
Unauthenticated requests receive a 401;
The logged‑in user’s id comes from the session, not from a client parameter.
5. Using Login Status in Pages: Show Only Relevant Buttons
Now adjust the frontend pages from the previous article: replace the hardcoded authorId = 1 with login‑dependent logic, and add UI decisions:
If not logged in, the new‑article page should prompt to log in;
On the article detail page, hide the comment form or prompt to log in when not authenticated.
5.1 Using useSession in Client Components
If you use next-auth/react, you can use the useSession hook on the client to get the login status.
Install:
pnpm add next-authInject SessionProvider in app/layout.tsx (abbreviated), then use it in comment-form.tsx and new/page.tsx.
Example with CommentForm:
'use client'
import { FormEvent, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useSession } from 'next-auth/react'
export default function CommentForm({ postId }: { postId: number }) {
const router = useRouter()
const { data: session, status } = useSession()
const [content, setContent] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
if (status === 'loading') {
return <p className="text-xs text-gray-500">Checking login status...</p>
}
if (!session?.user) {
return (
<p className="text-xs text-gray-500">
You must be logged in to post a comment.
</p>
)
}
async function handleSubmit(e: FormEvent) {
e.preventDefault()
setError(null)
setLoading(true)
try {
const res = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
})
const data = await res.json()
if (!res.ok) {
throw new Error(data.error || 'Comment failed')
}
setContent('')
router.refresh()
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-2">
<textarea
className="w-full rounded border px-3 py-2 text-sm min-h-[80px]"
placeholder="Write your comment..."
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
{error && <p className="text-xs text-red-500">{error}</p>}
<button
type="submit"
disabled={loading}
className="rounded bg-blue-600 px-3 py-1 text-xs text-white disabled:opacity-60"
>
{loading ? 'Submitting...' : 'Post Comment'}
</button>
</form>
)
}Similarly, the new‑article page can disable the form when not logged in and show a “Please log in” prompt.
6. Summary: Basic Integration of Prisma + Next.js + Auth
This article essentially did two things:
Based on the existing Prisma User model, integrated Auth.js/NextAuth to give Next.js login status;
In API Routes, used
auth()to get the current user, changing blog-demo’s “write operations” from a hardcodedauthorIdto “the real logged‑in user”.
From Prisma’s perspective, almost nothing changed:
The Schema gained a few fields/models related to auth;
Prisma Client usage remains the same, except you no longer trust the userId sent by the client; instead, you obtain it from the auth system’s session.
Follow on Google
Add HeyBinyang as a preferred source on Google
If you'd like to keep finding my updates through Google, you can mark this site as a preferred source and make it easier to spot in relevant reading flows.
SHARE
Share
Share this article.