Skip to content
< samuelsantana.dev />
Back to the Blog

Authentication and Security: JWT vs. Server-Side Sessions in the Next.js + NestJS Ecosystem

Samuel Santana
Published on July 19, 2026
NestJSNext.jsSecurity

If there is a topic in software engineering capable of generating more debates than the choice between spaces and tabs, it's authentication.

For years, the frontend community adopted an almost unanimous standard: the user logs in, the API returns a JSON Web Token (JWT), the frontend saves it in localStorage, and sends it in the header of subsequent requests. Simple, stateless, and extremely dangerous.

With the maturity of the ecosystem and the rise of full-stack frameworks like Next.js working alongside robust backends like NestJS, the way we architect security has changed. Next.js is no longer just an HTML generator; it has taken on the role of a BFF (Backend-For-Frontend).

In this article, we will dissect the JWT and Server-Side Sessions approaches, the hidden risks of each, and how to design a production-grade authentication architecture combining Next.js and NestJS.


The Original Sin: JWT in LocalStorage

Let's address the elephant in the room. Saving your access JWT in localStorage or sessionStorage exposes your application to XSS (Cross-Site Scripting) attacks.

If a single malicious script is injected into your frontend (whether through a compromised NPM dependency, a React sanitization flaw, or a third-party analytics script), that script has full access to localStorage. It can steal your user's token and impersonate them until the token expires.

The modern golden rule is: The client-side JavaScript code (the browser) must never have direct access to read the authentication token.


The Standard Approach: JWT in HttpOnly Cookies

To solve the XSS problem, the most widely adopted approach today is storing the JWT in a cookie with the HttpOnly, Secure, and SameSite=Strict flags.

The HttpOnly flag ensures that the JavaScript document.cookie cannot read the value. The token automatically travels in HTTP requests to the same domain.

The Next.js + NestJS Flow

In this scenario, Next.js acts as a secure intermediary:

  1. The client sends an email and password to a Server Action (or Route Handler) in Next.js.
  2. Next.js forwards these credentials to NestJS.
  3. NestJS validates them in the database and returns a signed JWT to Next.js.
  4. Next.js takes this JWT and sets an HttpOnly cookie in the user's browser.
// Next.js (Login Server Action)
import { cookies } from 'next/headers';

export async function loginAction(formData: FormData) {
  const response = await fetch('[https://api.yourdomain.com/auth/login](https://api.yourdomain.com/auth/login)', {
    method: 'POST',
    body: JSON.stringify(Object.fromEntries(formData)),
  });

  const { access_token } = await response.json();

  // The browser's JavaScript will NEVER see this token
  cookies().set('session_token', access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax', // or 'strict' depending on your subdomain architecture
    maxAge: 60 * 60 * 24 * 7, // 1 week
  });
}

On subsequent requests to the NestJS API, if Next.js and NestJS share the same root domain (e.g., app.yourdomain.com and api.yourdomain.com), the cookie travels automatically. Otherwise, Next.js (on the server) reads the cookie and injects the token into the Authorization: Bearer header before calling NestJS.

The Problem with JWT: Invalidation

JWT is amazing because it is stateless. NestJS doesn't need to hit the database to know if the user is logged in; it just verifies the cryptographic signature using @nestjs/passport and a JwtAuthGuard. This scales beautifully.

But this is also its greatest weakness. How do you log a user out immediately? If a user's account is compromised, or if an admin bans that user, the JWT will remain valid until it expires. You cannot "delete" a JWT that has already been issued.

If you start saving a blacklist of revoked tokens in Redis to check on every request, congratulations: you just turned your stateless architecture into a stateful one, losing the main advantage of JWT.


Server-Side Sessions: The Return of the King

This is where Server-Side Sessions shine again, especially in financial products, B2B SaaS, or applications where access control must be absolute and immediate.

Instead of placing a signed JSON with user data in the browser, the API (NestJS) creates a session record in a fast database (like Redis) and returns only an opaque identifier (a random Session ID, like a UUID string).

The Session Flow (Redis)

  1. Next.js sends credentials to NestJS.
  2. NestJS validates, generates a sessionId = 'abc-123', saves it in Redis (key: abc-123, value: { userId: 1, role: 'admin' }), and returns the ID.
  3. Next.js stores the sessionId in an HttpOnly cookie.

To protect routes in NestJS, instead of validating a cryptographic signature, the Guard fetches the session from Redis:

// NestJS (Session Guard Example with Redis)
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { RedisService } from './redis.service';

@Injectable()
export class SessionGuard implements CanActivate {
  constructor(private redis: RedisService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    // Next.js (BFF) extracted the cookie and sent it via header to the API
    const sessionId = request.headers['x-session-id']; 

    if (!sessionId) throw new UnauthorizedException();

    const sessionData = await this.redis.get(`session:${sessionId}`);
    if (!sessionData) throw new UnauthorizedException();

    // Injects the user into the request for controllers to use
    request.user = JSON.parse(sessionData);
    return true;
  }
}

Why is this powerful?

  • Total Control: Want to force a user logout from all devices? Just delete their keys in Redis. On the next click, the Guard will reject the request.
  • Hidden Payload: No user information travels in the cookie. With JWT, anyone intercepting the token can base64-decode it and read the data (though they cannot alter it without breaking the signature).

The Architectural Verdict: Which one to choose?

Like every answer in software engineering: it depends on what you are building.

Choose JWT (in HttpOnly Cookies) if:

  • You are building applications where the damage of a stolen short-lived token (e.g., 15 minutes) is low.
  • You need extreme scalability and want to save database/Redis calls on every request.
  • Your architecture involves dozens of microservices that need to validate identity in a decentralized manner.
  • Practical Tip: Implement the Refresh Token pattern (also in an HttpOnly cookie) to keep access JWTs very short-lived.

Choose Server-Side Sessions (Redis) if:

  • You are building fintechs, healthcare systems, or platforms with strict compliance rules.
  • You need the ability to revoke access instantly (e.g., a "Sign out of all other devices" button).
  • You need to actively track how many concurrent sessions a user has open.

The Fundamental Role of Next.js

Regardless of the choice in your API (NestJS), the architecture of Next.js with the App Router has drastically simplified credential management on the frontend.

By using React Server Components and Server Actions, you abstract browser complexity. The client doesn't need to know about JWTs, Axios interceptor libraries, or complex Refresh Token logic. The browser merely handles an invisible cookie, while the Next.js Node server does the heavy lifting of communicating with your backend infrastructure.

Your application's security is not just about the encryption algorithm you choose, but about the attack surface you expose. And in the modern frontend, the less the client JavaScript knows, the safer your user will be.

Comments

Loading comments...

Join the conversation

Sign in with your account to comment on this article.