Authentication Challenges in Next.js and NestJS Applications
How to handle authentication securely in a Next.js frontend with NestJS backend
I've worked with many different architectures: monoliths with multiple frontends, microservices with API gateways and frontends, and simple projects built entirely with Next.js.
Of course, these different architectures align with different levels of company maturity and revenue.
Today, I'm looking for the perfect architecture for businesses growing from 0 to $200K MRR—one that is both easy to adopt in the early stages and has no growth limitations post-PMF.
In our case, I want the developer experience of Next.js and the scalability of NestJS on the API side.
How can we combine these two technologies to create the best possible experience?
What's the problem with authentication in a Next/Nest app?
When building a modern web application with Next.js and NestJS, authentication presents several critical challenges:
-
Security Concerns: Do we need to handle authentication in both frontend and backend? Who can we delegate this responsibility to?
-
Session Handling: Managing user sessions, refresh tokens, and token expiration becomes complex when dealing with two separate frameworks.
-
API Route Protection: Both Next.js API routes and NestJS endpoints need protection, leading to potential duplication of authentication logic.
Direct API Communication vs. Next.js as an API Proxy
Let's compare using Next.js as an API Proxy to a more traditional direct communication with backend APIs, focusing mainly on authentication and API communication aspects.
Direct API Communication
In a traditional setup, a frontend application communicates directly with backend services or a general API. This could mean calling a monolithic API or multiple microservice endpoints from the client. The sequence diagram below shows a simplistic scenario where a client must be authenticated then fetch data from the backend.
sequenceDiagram
actor User
participant Client as Next.js Client
participant NextAuth as Auth.js
participant NestJS as NestJS API
participant OAuth as OAuth Provider
%% Login Flow
User->>Client: Click Login
Client->>NextAuth: Initiate Auth
NextAuth->>OAuth: Redirection
OAuth-->>NextAuth: Code Auth
NextAuth->>OAuth: Exchange Code/Token
OAuth-->>NextAuth: Access Token + User Info
NextAuth->>NextAuth: Generate JWT
NextAuth-->>Client: JWT Token
%% API Call Flow
User->>Client: User Action
Client->>NestJS: Direct Request + JWT Bearer
NestJS->>NestJS: Verify JWT
NestJS-->>Client: Response
Client-->>User: UI Update
After initial authentication, the client communicates directly with NestJS. JWT is stored client-side (typically in localStorage or sessionStorage). NestJS must validate each JWT independently. More "decoupled" architecture but more exposed on the client side.
Pros
- Fewer Infrastructure Layers: The client talks directly to the NestJS API, reducing the overall complexity of your setup.
- Lower Latency: Fewer hops mean potentially faster response times, as each request goes straight to the API without a proxy.
Cons
- Token Handling on the Client: The client must securely store and handle JWTs (potentially in browser storage), which can open security risks like token theft.
- CORS & Security: Direct calls from the browser to an external API may require careful CORS configuration, especially for cross-domain scenarios.
- Extra Burden on the Client: Handling refresh tokens, expiration checks, and secure storage all fall on the client, increasing complexity in front-end code.
Next.js as an API Proxy (Authenticated Proxy)
Using Next.js API routes as an authenticated proxy is a pattern where the Next.js server-side application acts as an intermediary between the client-side application (running in the browser) and your main backend API (NestJS in this case).
Instead of the client browser directly calling the NestJS API, all requests from the client are first routed to Next.js API routes. These routes handle authentication (e.g., using Auth.js to manage sessions and cookies) and then securely forward appropriate requests to the NestJS API. The NestJS API can then be configured to only accept requests from the Next.js proxy, enhancing security.
This approach centralizes authentication logic within the Next.js application, simplifying the client-side and providing a secure gateway to the backend services.
In our case, the server-side Next.js application will serve as this secure proxy between the Next.js application running in the browser and the API powered by NestJS.
sequenceDiagram
actor User
participant Client as Next.js Client
participant NextAuth as Auth.js
participant NextAPI as Next.js API Routes
participant NestJS as NestJS API
participant OAuth as OAuth Provider
%% Login Flow
User->>Client: Click Login
Client->>NextAuth: Initiate Auth
NextAuth->>OAuth: Redirection
OAuth-->>NextAuth: Code Auth
NextAuth->>OAuth: Exchange Code/Token
OAuth-->>NextAuth: Access Token + User Info
NextAuth->>NextAuth: Generate JWT
NextAuth-->>Client: JWT Cookie
%% API Call Flow
User->>Client: User Action
Client->>NextAPI: Request with JWT Cookie
NextAPI->>NextAuth: Verify JWT
NextAuth-->>NextAPI: JWT Valid
NextAPI->>NestJS: Secured Server to Server Call
NestJS-->>NextAPI: Response
NextAPI-->>Client: Data
Client-->>User: UI Update
All requests go through Next.js. The session is managed via a cookie on Next.js side. The client doesn't know the authentication details with NestJS. Next.js acts as an additional security layer.
Pros
- Centralized Auth Logic: The Next.js API Routes (acting as a proxy) manage sessions, tokens, and communication with the NestJS API. The client only deals with a session cookie.
- Client Simplicity: The front end rarely deals with raw tokens or security headers, since the Next.js proxy layer abstracts that away. It simply makes calls to its own backend API routes.
- Security & Session Management: Storing user session data on the server (via Auth.js in Next.js) can mitigate some risks associated with storing JWTs on the client. This also simplifies implementing more advanced auth flows like httpOnly cookies.
- Simplified Backend Exposure: The NestJS API doesn't need to be directly exposed to the public internet or handle CORS from browsers, as it only needs to accept requests from the Next.js proxy. This can reduce its attack surface.
Cons
- Additional Network Hop: Every request from the client must first go to the Next.js proxy, which then calls the NestJS API—adding a layer that can introduce latency if not optimized (though often this hop is within the same network/datacenter).
- Scaling Considerations: More moving parts require coordinated scaling strategies. Both the Next.js proxy layer and NestJS must be prepared to handle load.
Which implementation will be the easiest to learn and adopt for developers?
Direct to NestJS
Pros for learning:
- More straightforward architecture (direct API calls)
- Follows traditional SPA patterns
Cons for learning:
- More security considerations to understand
- Need to manage tokens client-side
- CORS configuration can be tricky
- Need to understand JWT handling in depth
- More boilerplate code in components for auth headers
Next.js as an API Proxy
Pros for learning:
- More familiar pattern for Next.js developers (using API routes as backends)
- Simpler client-side code (just fetch to /api/ on the same domain)
- Less security concerns for the client to manage directly (tokens handled server-side by Next.js)
- Follows Next.js documentation patterns for API routes
- Fewer configurations to manage (CORS, token storage, etc.)
- Clear separation of concerns: client talks to Next.js, Next.js talks to NestJS.
Cons for learning:
- A NestJS developer needs to understand this proxy pattern and how requests flow through Next.js before reaching their API.
- Debugging can involve an extra step if issues arise in the proxy layer.
Recommendation
I would recommend the Next.js as an API Proxy approach for the boilerplate because:
- Lower Learning Curve:
- Developers can start with a simpler mental model
- Security best practices are mostly handled by the framework
- Fewer concepts to understand initially
- Better Documentation Support:
- Aligns with Next.js documentation examples
- More community resources available
- Easier to find solutions to common problems
- Safer Defaults:
- Less room for security mistakes
- Harder to accidentally expose sensitive information
- Better error handling out of the box
- Maintainability:
- Easier to add features later
- Simpler debugging for client-side issues (as they only interact with Next.js)
- Clearer code organization and responsibility.
Conclusion
Using Next.js as an API Proxy provides an approach that effectively balances the simplicity and developer experience of Next.js with the robustness of a separate NestJS backend. By positioning Next.js API routes as an intermediary layer that handles authentication and securely forwards requests to the NestJS API, authentication and security logic are centralized within the Next.js application. This keeps client-side code clean and simple. This setup reduces the risks associated with storing tokens in the browser and makes it easy to introduce new features as the product evolves.
For a growing business, this pattern allows for a quick start with an "all-in-one" feel for the frontend team (Next.js app + its API routes) while enabling the backend (NestJS) to scale independently and remain securely shielded. Although this approach introduces an additional layer, it brings clarity, improved security posture, and flexibility. Ultimately, for those seeking a balance between fast implementation, maintainable code, and an architecture that supports long-term growth, using Next.js as a secure API proxy remains an excellent choice at this stage of maturity.