
You've set up your Next.js application as your frontend, carefully crafting the UI and implementing the routing. But now you're faced with a crucial challenge: how do you properly handle authentication? Should you add another authentication layer? Is storing JWT in localStorage secure enough? And how does this all work with a separate backend like Nest.js?
If you're feeling overwhelmed by these questions, you're not alone. Many developers struggle with implementing secure authentication when using Next.js as a frontend-only framework. The good news is that there's a clear path forward, and I'll show you exactly how to implement it.
Why Next.js as a Frontend-Only Framework?
Next.js has evolved beyond its initial server-side rendering roots. While it excels at SSR and SSG, many developers are now using it purely for its frontend capabilities:
File-based routing: Intuitive and maintainable route management
Built-in optimization: Automatic code splitting and image optimization
Rich ecosystem: Seamless integration with popular CSS frameworks and tools
Developer experience: Hot reloading and excellent debugging tools
However, when using Next.js this way, authentication becomes a critical concern that needs careful consideration.
The Authentication Challenge
The most common pain points developers face when implementing authentication in a Next.js frontend include:
Security Concerns: Many developers default to storing JWT tokens in localStorage, which exposes them to XSS attacks.
Token Management: Implementing token rotation and refresh mechanisms can become complex, especially when dealing with server-side actions and cookies.
Integration Complexity: Connecting Next.js with backend frameworks like Nest.js while maintaining secure sessions can be overwhelming.
Let's address these challenges one by one with practical, secure solutions that you can implement today.
Setting Up Secure Authentication
1. Project Structure
First, let's set up our project structure properly. A clean separation between frontend and backend is crucial:
your-project/
├── frontend/ # Next.js application
│ ├── src/
│ ├── public/
│ └── package.json
├── backend/ # Nest.js application
│ ├── src/
│ └── package.json
└── package.json # Root package.json for managing workspaces
2. Implementing JWT Authentication
Instead of storing tokens in localStorage, we'll use httpOnly cookies for enhanced security. Here's how to set it up:
// frontend/src/services/auth.ts
import axios from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true // Important for cookie handling
});
export const login = async (credentials: LoginCredentials) => {
const response = await api.post('/auth/login', credentials);
return response.data;
};
On the Nest.js side:
// backend/src/auth/auth.controller.ts
@Post('login')
async login(@Res({ passthrough: true }) response: Response) {
const token = await this.authService.generateToken();
response.cookie('jwt', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
return { success: true };
}
3. Token Refresh Implementation
One of the most challenging aspects is implementing token refresh. Here's a robust solution using axios interceptors:
// frontend/src/services/axios.ts
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
await api.post('/auth/refresh');
return api(originalRequest);
} catch (refreshError) {
// Redirect to login if refresh fails
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
Protected Routes and Authentication Flow
1. Creating Protected Routes
To secure your routes in Next.js, implement a higher-order component that checks for authentication:
// frontend/src/components/ProtectedRoute.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useAuth } from '@/hooks/useAuth';
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, loading, router]);
if (loading) {
return <div>Loading...</div>;
}
return isAuthenticated ? <>{children}</> : null;
}
2. Authentication Hook
Create a custom hook to manage authentication state:
// frontend/src/hooks/useAuth.ts
import { create } from 'zustand';
interface AuthStore {
isAuthenticated: boolean;
loading: boolean;
setAuthenticated: (value: boolean) => void;
setLoading: (value: boolean) => void;
}
export const useAuth = create<AuthStore>((set) => ({
isAuthenticated: false,
loading: true,
setAuthenticated: (value) => set({ isAuthenticated: value }),
setLoading: (value) => set({ loading: value }),
}));
3. API Route Protection
In your Nest.js backend, implement middleware to verify JWT tokens:
// backend/src/auth/jwt.guard.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err: any, user: any) {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
Apply the guard to your protected routes:
// backend/src/users/users.controller.ts
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
Best Practices and Common Pitfalls
Security Considerations
Always Use httpOnly Cookies
Never store sensitive information in localStorage
Set appropriate cookie flags (secure, sameSite)
Implement CSRF protection for additional security
Implement Rate Limiting
// backend/src/auth/rate-limit.guard.ts import { ThrottlerGuard } from '@nestjs/throttler'; @Injectable() export class RateLimitGuard extends ThrottlerGuard { protected errorMessage = 'Too Many Requests'; }
Handle Token Expiration Gracefully
Implement automatic token refresh
Clear authentication state on logout
Redirect to login page when refresh fails
Common Pitfalls to Avoid
Cross-Origin Issues
Ensure proper CORS configuration:
// backend/src/main.ts app.enableCors({ origin: process.env.FRONTEND_URL, credentials: true, });
Token Management
Don't store tokens in localStorage
Implement proper token rotation
Handle token refresh race conditions
Error Handling
Implement proper error boundaries
Provide meaningful error messages
Log authentication failures for monitoring
Conclusion
Using Next.js as a frontend-only framework with Nest.js as your backend can create a powerful and secure application architecture. By following these best practices and implementing proper authentication mechanisms, you can avoid common security pitfalls while maintaining a clean separation of concerns.
Remember to:
Use httpOnly cookies for token storage
Implement proper token refresh mechanisms
Protect your routes both on frontend and backend
Handle errors and edge cases gracefully
For more information and advanced configurations, refer to the Next.js documentation and Nest.js authentication guide.