This document outlines the strict flow and architectural rules for the Next.js Backend-For-Frontend (BFF) pattern. It specifically answers how data flows from the React UI all the way to the NestJS microservices.
1. React Component → 2. Redux Thunk → 3. Next.js API Route → 4. Next.js Service Layer → 5. NestJS Microservice
What to send: The React components and Redux thunks should only send the literal data required for the action.
- POST/PUT/PATCH (Mutations): Send the data in the JSON
body. Example:{ email: "x", password: "y" }or{ content: "new post" }. - GET (Queries): Send the data in the URL Search Params (e.g.,
/api/posts?page=2) or URL Path (e.g.,/api/posts/123).
How tokens are handled:
- CRITICAL RULE: The client UI never manually attaches
accessTokenor refreshToken to theAuthorizationheader or body. - Because the tokens are stored as
HttpOnlycookies, the browser automatically and securely attaches them to every single request made to/api/*.
Purpose: These are the "traffic cops". They receive the browser request, extract the data, check auth, and pass it to the brains.
Method Handling: Next.js uses file-based routing. You export functions named export async function GET, POST, PUT, etc. The browser's fetch method dictates which function runs.
Extracting Data in the Route Handler:
- Body (POST/PUT):
const body = await request.json(); - Query (GET):
const { searchParams } = new URL(request.url); const page = searchParams.get('page'); - Params (Dynamic Routes): Handled via the second argument:
export async function GET(req, { params }) { const id = params.id; }
Connecting to the Service: The Route Handler must never contain heavy business logic or raw Axios calls. It extracts the data using the rules above, runs authGuard(request) to get the AuthContext (who the user is), and directly passes them to the Service Layer:
const { userId } = await authGuard(request);
const result = await PostsService.createPost(userId, body);
return NextResponse.json(result.data, { status: result.error ? 400 : 200 });Purpose: This is the bridge between Next.js and NestJS. It holds the pre-configured Axios clients and normalizes all results.
Input Format:
It takes strictly typed arguments. E.g., createPost(userId: string, targetUserId: string, payload: CreatePostDto).
Format Out (Success): It should return a predictable tuple or result object so the Route Handler doesn't need to try/catch:
return { data: response.data, error: null };Format Out (Error):
If the Axios call fails, the Service Layer catches it, extracts the CustomErrorResponseDto sent by your NestJS backend, and formats it cleanly:
catch (error) {
// Extracts the exact error emitted by NestJS
return { data: null, error: { message: "Invalid Post", errorCode: "VALIDATION_FAILED", statusCode: 400 } };
}- Junior Mistake: Passing tokens in the body
- Wrong:
fetch('/api/auth/logout', { body: JSON.stringify({ refreshToken }) }) - Right: The browser sends the cookie automatically. The Route Handler reads it via
cookies().get('refreshToken').
- Wrong:
- Junior Mistake: "Fat" Route Handlers
- Wrong: Putting
try/catch,axios.post(), and error mapping directly insidesrc/app/api/auth/login/route.ts. - Right: The Route Handler should just be 10 lines of code. It delegates to
AuthService.login().
- Wrong: Putting
- Junior Mistake: Leaking Axios Errors to the UI
- Wrong: Catching an error and doing
return NextResponse.json({ error: err }). This dumps a giant, confusing network object to the frontend. - Right: The Service layer catches the Axios error, shapes it to match
CustomErrorResponseDto, and the Route Handler returns exactly that shape so the UI can checkif (error.errorCode === 'EXPIRED').
- Wrong: Catching an error and doing
- Junior Mistake: Server Components calling
/api/- Wrong: In a Next.js React Server Component (
app/dashboard/page.tsx), doingfetch('http://localhost:3000/api/profile'). - Right: Server Components are already on the server! They should bypass the Route Handler completely and just call
ProfileService.getProfile(userId).
- Wrong: In a Next.js React Server Component (