• Toggle theme

Β© 2026 Moises Lugo. All rights reserved.

 Next-Auth v5 and Next.js

Next-Auth v5 and Next.js

πŸ‘€ Moises Lugo🏷️ Next.js , NextAuth , Prisma, Server ActionsπŸ“… Oct 26, 2025
⏱️ 5 min read

Modern Auth with Next-Auth v5 and Next.js 14 (Updated)

Auth has always been one of the most annoying parts of any web app.
But with Next-Auth v5, things got way easier and cleaner β€” especially if you’re using Next.js 14 with the new App Router.

Now, we separate auth config and the NextAuth handler, which makes the code cleaner, easier to maintain, and more scalable.


🧩 Stack I’m using

  • Next.js 14 (App Router)
  • Next-Auth v5
  • Prisma for database
  • PostgreSQL
  • TypeScript
  • Zod + React Hook Form
  • Shadcn/UI

βš™οΈ Basic setup

Install dependencies:

npm install next-auth @next-auth/prisma-adapter bcryptjs @prisma/client

Update your schema.prisma User model:

model User {
  id            String   @id @default(cuid())
  name          String?
  username      String?  @unique
  email         String?  @unique
  password      String
  emailVerified DateTime?
  avatarUrl     String?
  bio           String?
  role          String   @default("USER")
  accounts      Account[]
  sessions      Session[]
}

Run migrations:

npx prisma migrate dev

πŸ” Auth config

auth.config.ts

import { type NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import prisma from '@lib/prisma'
import bcrypt from 'bcryptjs'
import { PrismaAdapter } from '@next-auth/prisma-adapter'

export const authConfig: NextAuthConfig = {
  adapter: PrismaAdapter(prisma),
  providers: [
    Credentials({
      name: 'Credentials',
      id: 'credentials',
      credentials: {
        identifier: { label: 'Email or Username', type: 'text', placeholder: 'user or user@gmail.com' },
        password: { label: 'Password', type: 'password', placeholder: '******' },
      },
      async authorize(credentials) {
        const { password, identifier } = credentials
         (!password || !identifier)   ()

         user =  prisma..({
          : { : [{ : identifier }, { : identifier }] },
        })
         (!user)   ()

         match =  bcrypt.(password, user.)
         (!match)   ()

         {
          : user.,
          : user.,
          : user.   | ,
          : user.!,
          : user.,
          : user.,
          : ,
        }
      },
    }),
  ],
  : {
     () {
       (user) {
        token. = user.
        token. = user.
        token. = user.
        token. = user.
      }
       token
    },
     () {
       (token) {
        session. = {
          : token.  ,
          : token.  ,
          : token.  ,
          : token.  ,
          : token.   | ,
          : ,
        }
      }
       session
    },
  },
}

auth.ts

import NextAuth from 'next-auth'
import { authConfig } from './auth.config'

export const { handlers, auth, signIn, signOut } = NextAuth({
  ...authConfig,
  pages: { signIn: '/login' },
  session: { strategy: 'jwt', maxAge: 7 * 24 * 60 * 60 },
  jwt: { maxAge: 7 * 24 * 60 * 60 },
  secret: process.env.AUTH_SECRET,
})

🧠 Using useSession

'use client'

import { useSession, signIn, signOut } from "@/app/auth"

export default function Header() {
  const { data: session } = useSession()

  return (
    <header className="flex items-center justify-between p-4 border-b">
      {session ? (
        <>
          <p>Hi, {session.user?.username}</p>
          <button onClick={() => signOut()}>Logout</button>
        </>
      ) : (
        <button onClick={() => signIn()}>Login</button>
      )}
    </header>
  )
}

🧭 Protecting server routes

import { auth } from "@/app/auth"
import { redirect } from "next/navigation"

export default async function DashboardPage() {
  const session = await auth()
  if (!session) redirect("/login")

  return <h1>Welcome back, {session.user?.username}!</h1>
}

πŸ† Benefits of this approach

  1. Separation of concerns – Auth logic is separate from NextAuth handler.
  2. Easier maintenance – Adding new providers or updating callbacks is simple.
  3. Better security – Centralized config and JWT options.
  4. Scalability – Works well if you want to add roles, permissions, or multiple login methods.

πŸš€ Conclusion

This setup gives you:

  • Full server & client session management
  • Credentials login ready with Prisma & bcrypt
  • Clean, maintainable, and scalable code

No more messy auth files or hacks πŸ˜…
Perfect for modern Next.js 14 apps using Next-Auth v5.

if
throw
new
Error
'Missing credentials'
const
await
user
findFirst
where
OR
email
username
if
throw
new
Error
'User not found'
const
await
compare
password
if
throw
new
Error
'Invalid password'
return
id
id
email
email
role
role
as
'ADMIN'
'USER'
username
username
avatarUrl
avatarUrl
bio
bio
emailVerified
null
callbacks
async
jwt
{ token, user }
if
id
id
email
email
role
role
username
username
return
async
session
{ session, token }
if
user
id
id
as
string
email
email
as
string
username
username
as
string
avatarUrl
avatarUrl
as
string
role
role
as
'ADMIN'
'USER'
emailVerified
null
return