Why can't we just use JWTs in Next.js?

Garvit DadheechGarvit Dadheech
3 min read

In traditional React + Express apps, it's common to use JWTs (JSON Web Tokens) stored in localStorage for authentication. So when working with Next.js, it's natural to ask:

"Why can't I just use JWTs in Next.js like I do in React?"

The short answer: you can, but you'll break one of Next.js’s core powers — Server-Side Rendering (SSR)

The main reason is – We can't really send the JWT token in the very first request that comes from the browser.

Let’s understand this.

Cookies Are the Norm in Next.js

In a framework like Next.js, most people use cookies for authentication — either through next-auth, or services like Auth0 or Clerk.

But when you try to manually use JWT stored in localStorage, things fall apart — especially with SSR (Server Side Rendering).

So What Actually Breaks?

Let’s assume JWT works perfectly. Say you have a /profile page where you want to show the user’s info from their JWT.

You store the token in localStorage, then on /profile, fetch and decode the token, and get the user’s info.

Seems fine, right?

But here’s how things work under the hood in Next.js:

  1. The browser sends the very first request to your server — SSR kicks in.

  2. Server tries to render /profile.

  3. But where is the JWT? It’s in localStoragewhich the server can’t access.

  4. So server sends a blank or loading page.

  5. After hydration, your client-side JS runs, grabs the token from localStorage, and then makes another request to actually get the user’s data.

In short: you don’t get the user’s data on the first request — you miss the SSR magic. Actually to get the user’s data you made two requests, both from your browser.

Why Cookies Work

If you store the JWT inside a cookie (preferably an HTTP-only cookie), that cookie gets sent automatically in every request – including that very first SSR request.

Now:

  • Server sees the cookie immediately

  • It can decode it

  • It renders the full personalized page before sending it to the client

That’s how SSR is supposed to work.

Code Examples - with JWTs

/signin/route.tsx

import jwt from "jsonwebtoken";
import { NextRequest, NextResponse } from "next/server";

const SECRET_KEY = "top secret";

export async function POST(req: NextRequest) {
    const body = await req.json();

    const userId = "123456"

    const token = jwt.sign({ userId }, SECRET_KEY);

    return NextResponse.json({ token });
}

/profile/route.tsx

import jwt from "jsonwebtoken";
import { NextRequest, NextResponse } from "next/server";

export function GET(req: NextRequest) {
    const headers = req.headers;
    const authorizationHeader = headers.get("authorization");
    const decoded = jwt.decode(authorizationHeader, "SECRET_KEY");
    const userId = decoded.userId;

    return NextResponse.json({
        username: "new user"
    });
}

/profile/page.tsx

"use client"
import axios from "axios";
import { useEffect, useState } from "react";

export default function Profile() {
    const [username, setUsername] = useState("");

    useEffect(() => {
        axios.get("http://localhost:3000/api/profile", {
            headers: {
                authorization: localStorage.getItem("token")
            }
        }).then(res => {
            setProfilePicture(res.data.username);
        });
    }, []);

    return <div>{username}</div>;
}

Why This Fails

  • localStorage is client-only.

  • On the very first request, the server doesn’t get the token.

  • SSR fails to personalize.

  • Extra requests are needed.

Final Thoughts

If you’re using Next.js and want SSR (which is literally why most people use it), don’t rely on localStorage for auth.

Either:

  • Store JWTs in HTTP-only cookies

  • Or use next-auth, Auth0, Clerk, etc.

That way, you can access the auth info in the very first request, and render full pages server-side – as intended.

0
Subscribe to my newsletter

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

Written by

Garvit Dadheech
Garvit Dadheech

I am a Full Stack, DevOps, and Web3 Developer with expertise in building end-to-end applications. As a fast learner, I quickly adapt to different technology stacks, ensuring the delivery of robust software solutions. I specialize in creating scalable web applications tailored to meet client needs and drive innovation.