How to Implement Full Authentication in Next.js with JWT and PostgreSQL

Vinay PatelVinay Patel
3 min read

In this article, I'll walk you through how I implemented a complete authentication system using:

  • PostgreSQL for user data

  • JWT for secure authentication

  • React Context API for managing auth state

  • Middleware for protected routes

  • A dynamic navbar that displays the logged-in user's name

  • Errors I encountered—and how I fixed them!


Step 1: Setting Up PostgreSQL and Prisma

I started by initializing PostgreSQL and integrating it with Prisma ORM.

Prisma Schema Example:

model User {
  id        String   @id @default(uuid())
  name      String
  email     String   @unique
  password  String
  createdAt DateTime @default(now())
}

Then ran:

npx prisma migrate dev --name init

This generated the tables in my PostgreSQL database.


Step 2: Creating Auth Utilities with JWT

I created lib/auth.ts to handle:

  • Password hashing with bcrypt

  • Token generation with jsonwebtoken

  • Token verification

Example:

import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';

export const generateToken = (user: any) => {
  return jwt.sign({ id: user.id, email: user.email, name: user.name }, process.env.JWT_SECRET!, {
    expiresIn: '1d',
  });
};

export const verifyToken = (token: string) => {
  return jwt.verify(token, process.env.JWT_SECRET!);
};

Step 3: Backend Login Route

I created /api/auth/login/route.ts to validate credentials and set the token in cookies.

Key steps:

  • Compare hashed passwords

  • On success: generate JWT and send as cookie

  • Return user data

Error Faced:

"Login failed" on every attempt

Solution:

Ensured credentials: 'include' was added in frontend fetch() to allow cookies to persist.


Step 4: Frontend Auth Context

I created a global AuthContext to manage login, logout, register, and current user state using useEffect.

useEffect(() => {
  async function loadUser() {
    const res = await fetch('/api/auth/me');
    if (res.ok) {
      const data = await res.json();
      setUser(data.user);
    }
    setLoading(false);
  }
  loadUser();
}, []);

Step 5: Protecting Routes with Middleware

Used Next.js middleware to check if user has token for protected pages like /cart.

import { NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const token = req.cookies.get('token')?.value;
  if (!token && req.nextUrl.pathname.startsWith('/cart')) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
  return NextResponse.next();
}

Step 6: Testing Login + Redirect to Cart

After login, user redirected to /cart:

window.location.href = '/cart';

This worked only after fixing credentials: 'include' in the fetch call.


Step 7: Fetch Logged-in User in /api/auth/me

Here’s the important route to get the user from token:

import { cookies } from 'next/headers';
import { verifyToken } from '../../../../lib/auth';

export async function GET() {
  const token = cookies().get('token')?.value;

  if (!token) return NextResponse.json({ user: null });

  try {
    const decoded = verifyToken(token);
    return NextResponse.json({ user: decoded });
  } catch (e) {
    return NextResponse.json({ user: null });
  }
}

Error Encountered:

Export verifyToken doesn't exist in target module

Fix:

I forgot to export verifyToken from lib/auth.ts. Added:

export const verifyToken = ...

Step 8: Display Logged-in User Name in Navbar

Finally, I updated the Navbar:

const { user, isAuthenticated, loading } = useAuth();

{!loading && isAuthenticated && (
  <span className="text-sm font-medium text-gray-700">Hi, {user?.name}</span>
)}

Final Result

  • PostgreSQL stores secure user data

  • JWT authenticates users and persists session

  • Middleware protects sensitive routes

  • Navbar shows real-time user name

  • Auth state persists with React Context + /api/auth/me


Bugs I Solved

ErrorFix
verifyToken not exportedExported it from lib/auth.ts
Login not redirectingUsed credentials: 'include' in fetch
Navbar not updatingUsed useEffect to fetch user on load
{"user":{}}Fixed logic in /api/auth/me to return user properly

Have questions or improvements? Drop them in the comments.

0
Subscribe to my newsletter

Read articles from Vinay Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vinay Patel
Vinay Patel

Hi, My name is Vinay Patel, and I'm an aspiring software developer.