Integration Guide¶
Document Type: Frontend-Backend Communication Generated: 2026-02-12 | Last Verified: 2026-03-26 | Scan Level: Exhaustive | Workflow: document-project v1.2.0
Overview¶
The frontend (Next.js 15) communicates with the backend (Express.js) via REST API calls. Client-side components use Axios with interceptors; server-side components (RSC) use native fetch() with Next.js cache tags. Authentication is handled through JWT tokens stored in a JS-accessible cookie (not httpOnly).
Communication Flow¶
┌──────────────────────┐ ┌──────────────────────┐
│ Frontend │ │ Backend │
│ (Next.js 15) │ │ (Express.js) │
├──────────────────────┤ ├──────────────────────┤
│ │ │ │
│ Client Components │ Axios + Bearer │ │
│ (139 RQ hooks, │─────────────────────>│ Express Routes │
│ 31 hook files) │ │ │
│ │<─────────────────────│ JSON Response │
│ │ │ │
│ Server Components │ fetch() + tags │ │
│ (RSC) │─────────────────────>│ Public endpoints │
│ │<─────────────────────│ │
│ │ │ │
│ Cookie: "token" │ (not httpOnly) │ JWT in response │
│ (JS-readable) │<─────────────────────│ body, not header │
└──────────────────────┘ └──────────────────────┘
Environment Configuration¶
Frontend (.env.local)¶
# Public API URL (used in browser / client components)
NEXT_PUBLIC_API_URL=http://localhost:5500/api/v1
# Server-side API URL (used in server components / RSC)
API_URL=http://localhost:5500/api/v1
# Stripe (client-side)
STRIPE_PUBLISHABLE_KEY=pk_test_...
Backend (.env)¶
# Client URL for CORS and redirects
CLIENT_URL=http://localhost:3000
# CORS origins (JSON array)
ALLOWED_ORIGINS=["http://localhost:3000","http://localhost:3001"]
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Email (one of: nodemailer, resend, brevo)
EMAIL_PROVIDER=nodemailer
# Storage (one of: cloudinary, aws, local)
STORAGE_PROVIDER=cloudinary
Authentication Integration¶
1. Login Flow¶
Frontend Backend
│ │
│ POST /auth/login │
│ {email, password, rememberMe} │
├────────────────────────────────────>│
│ │ Validate credentials
│ │ Check if 2FA enabled
│ │
│ ┌─ If 2FA required ──────────────┐│
│ │ Return early: {twoFactor: true} ││
│ │ NO cookie set, NO token ││
│ │ Frontend redirects to 2FA page ││
│ └────────────────────────────────┘│
│ │
│ ┌─ Normal login ────────────────┐ │
│ │ {token, user} │ │
│ │<────────────────────────────────┤
│ │ │
│ │ setCookie("token", value, { │
│ │ maxAge: 30 days (rememberMe) │
│ │ or 7 days (default), │
│ │ sameSite: "strict", │
│ │ secure: NODE_ENV==="production",│
│ │ NOT httpOnly │
│ │ }) │
│ │ │
│ │ dispatch(setAuth({token})) │
│ │ — sets isAuthenticated + token │
│ │ — does NOT set user in Redux │
│ │ │
│ │ window.location.href="/dashboard"│
│ │ (hard redirect, full page │
│ │ reload — not Next.js router) │
│ └─────────────────────────────────┘
│ │
2. Frontend Cookie Management¶
// On login success (login page)
import { setCookie } from "cookies-next";
const handleLogin = async (credentials) => {
const response = await authAPI.login(credentials);
// 2FA early return — no cookie, no Redux update
if (response.twoFactor) {
// redirect to 2FA verification page
return;
}
// Store token in cookie (JS-accessible, NOT httpOnly)
setCookie("token", response.token, {
maxAge: credentials.rememberMe
? 60 * 60 * 24 * 30 // 30 days
: 60 * 60 * 24 * 7, // 7 days
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
path: "/",
});
// Update Redux — sets isAuthenticated + token, but NOT user
dispatch(setAuth({ token: response.token }));
// Hard redirect (full page reload, not Next.js router.push)
window.location.href = "/dashboard";
};
3. Auth Initializer (App Load)¶
// components/providers/auth-initializer.jsx
"use client";
export default function AuthInitializer({ children }) {
const dispatch = useDispatch();
useEffect(() => {
// initializeAuth() reads the "token" cookie
// Sets isAuthenticated + token in Redux, but NOT user
// User data is fetched separately by useProfile()
dispatch(initializeAuth());
// Wire Redux dispatch to Axios interceptor for logout-on-401
setStoreDispatch(dispatch);
}, [dispatch]);
return children;
}
Key detail:
initializeAuth()only restores the token andisAuthenticatedflag. The actual user object (with role, permissions, etc.) is loaded separately byuseProfile(), which callsGET /users/profile. This means there is a brief window after page load whereisAuthenticatedis true butuseris null.
4. Axios Interceptor (Request)¶
// interceptors/axiosInstance.js
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true, // Send cookies with requests
});
api.interceptors.request.use((config) => {
const token = getCookie("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
5. Axios Interceptor (Response Error)¶
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401 || error.response?.status === 403) {
// Clear cookie
deleteCookie("token");
// Clear Redux state
if (storeDispatch) {
storeDispatch(logout());
}
// Clear React Query cache
// (queryClient.clear() called in logout action)
// Redirect to login
if (typeof window !== "undefined") {
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
Known Issue: 403 (Forbidden) triggers a full logout identical to 401 (Unauthorized). A 403 means the user is authenticated but lacks permission for that resource — it should NOT destroy the session. This causes users to be logged out when they simply lack a specific permission.
6. Logout Flow¶
1. deleteCookie("token") — remove JWT cookie
2. dispatch(logout()) — clear Redux auth state
3. queryClient.clear() — wipe all React Query cache
4. redirect to /login — via window.location.href
Middleware Protection¶
Next.js Middleware¶
// middleware.js
import { NextResponse } from "next/server";
export async function middleware(req) {
const token = req.cookies.get("token")?.value;
const { pathname } = req.nextUrl;
// Protect dashboard routes
if (pathname.startsWith("/dashboard")) {
if (!token) {
return NextResponse.redirect(new URL("/login", req.url));
}
}
// Redirect authenticated users away from auth pages
if (token && ["/login", "/register"].includes(pathname)) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/login", "/register", "/dashboard/:path*"],
};
Note: Middleware only checks for cookie existence — it does not validate the JWT. An expired or malformed token will pass middleware but fail on the first API call, triggering the Axios 401 interceptor.
Data Fetching Patterns¶
1. React Query Hooks (Client Components)¶
Scale: 139 React Query hooks across 31 files.
// api/users/index.js
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import api from "@/interceptors/axiosInstance";
// Fetch current user profile
export const useProfile = () => {
return useQuery({
queryKey: ["user", "profile"],
queryFn: async () => {
const response = await api.get("/users/profile");
return response.data.data;
},
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
// Fetch users list
export const useUsers = (params) => {
return useQuery({
queryKey: ["users", params],
queryFn: async () => {
const response = await api.get("/users", { params });
return response.data;
},
// staleTime: CACHE_TIME (configurable, in hours)
});
};
// Create user mutation
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data) => api.post("/users", data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
};
React Query Provider Configuration:
- No retry on 401 or 403 status codes
- staleTime varies: 5 minutes for profile, CACHE_TIME hours for list queries
- Provider wraps the app in the root layout
2. Server-Side Fetching (Server Components / RSC)¶
Server components use native fetch() with Next.js cache tags for on-demand revalidation.
7 cache tags in use:
| Tag | Used For |
|---|---|
| blogs | Blog listing pages |
| blog-details | Individual blog post pages |
| dynamic-page | CMS dynamic pages |
| navigation-links | Header/footer/sidebar menus |
| packages-data | Pricing/subscription packages |
| settings-group | Site settings by group/prefix |
| all-settings | All settings (global fetch) |
// Server component fetching with cache tags
async function getBlogs() {
const response = await fetch(`${process.env.API_URL}/blogs`, {
next: { tags: ["blogs"] },
});
return response.json();
}
export default async function BlogsPage() {
const { data: blogs } = await getBlogs();
return <BlogList blogs={blogs} />;
}
3. Dynamic Metadata¶
// app/layout.js
async function fetchMetaData() {
const response = await fetch(
`${process.env.API_URL}/settings/admin/prefix`,
{ next: { tags: ["settings-group"] } }
);
return response.json();
}
export async function generateMetadata() {
const settings = await fetchMetaData();
return {
title: settings?.admin?.meta_title || "SaaS App",
description: settings?.admin?.meta_description,
};
}
Cache Revalidation System¶
Server-side cache (Next.js fetch tags) is revalidated on-demand via server actions.
Server Actions¶
// src/actions/revalidate.js
"use server";
import { revalidateTag } from "next/cache";
// Revalidate a single tag
export async function revalidate(tag) {
revalidateTag(tag);
}
// Revalidate multiple tags at once
export async function revalidateBulk(tags) {
tags.forEach((tag) => revalidateTag(tag));
}
// Nuclear option: revalidate all 7 tags
export async function revalidateAll() {
const allTags = [
"blogs", "blog-details", "dynamic-page",
"navigation-links", "packages-data",
"settings-group", "all-settings",
];
allTags.forEach((tag) => revalidateTag(tag));
}
Revalidation Triggers¶
| Mutation | Tag(s) Revalidated |
|---|---|
| Blog create/update | blogs |
| Blog detail change | blog-details |
| Settings update | settings-group |
| Menu item changes | navigation-links (via revalidateNavigationLinks()) |
| Admin manual purge | All tags (via /dashboard/settings/revalidate UI) |
Admin Revalidation UI¶
The dashboard provides a manual cache purge interface at /dashboard/settings/revalidate, allowing admins to selectively or fully invalidate cached server-side data.
Permission Checking¶
Backend: Route Permission¶
// Backend middleware checks permissions automatically
router.get("/users", auth(), hasPermission(), UserController.getUsers);
// Requires: /users permission for GET
// Requires: /users/create for POST, /users/edit for PATCH, etc.
Frontend: UI Permission¶
// hooks/useCheckPermission.js
import { useSelector } from "react-redux";
export const useCheckPermission = (permission) => {
const { user } = useSelector((state) => state.auth);
if (!user) return false;
if (user.role === "super_admin") return true;
return user.permissions?.includes(permission);
};
// Usage in component
function AdminButton() {
const canManageUsers = useCheckPermission("/users/create");
if (!canManageUsers) return null;
return <button>Add User</button>;
}
Dynamic Navigation¶
// Dashboard sidebar filters menu items by permission
const { data: links } = useNavigationLinks("dashboard-sidebar-top");
// Backend filters items based on user's permissions
// MenuItem.permission field is checked against user's permissions
File Upload Flow¶
Frontend Backend Storage Provider
│ │ │
│ POST /uploads/image │ │
│ FormData with file │ │
├───────────────────────────────>│ │
│ │ multer (memory storage) │
│ │ Validate: image-only types │
│ │ │
│ │ StorageFactory resolves: │
│ │ ┌─ CloudinaryStorage │
│ │ │ (auto WebP conversion) │
│ │ ├─ AwsStorage │
│ │ └─ LocalStorage │
│ │ │
│ │ Upload to provider │
│ ├─────────────────────────────>│
│ │ │
│ │ {public_id, url} │
│ │<─────────────────────────────┤
│ │ │
│ {url, publicId} │ │
│<───────────────────────────────┤ │
│ │ │
│ Use URL in form │ │
// Frontend: uploadFile.js
const uploadFile = async (file) => {
const formData = new FormData();
formData.append("file", file);
// Endpoint: POST ${NEXT_PUBLIC_API_URL}/uploads/image
const response = await api.post("/uploads/image", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data.data.url;
};
Backend Storage Providers (factory pattern via STORAGE_PROVIDER env):
- CloudinaryStorage: Auto-converts uploads to WebP format
- AwsStorage: S3 bucket upload
- LocalStorage: Saves to local filesystem
- All providers enforce image-only file types
Payment Integration¶
Stripe Checkout¶
Frontend Backend Stripe
│ │ │
│ POST /payments/checkout │ │
│ {packageId, type} │ │
├──────────────────────────>│ │
│ │ PaymentFactory.ts │
│ │ -> StripeService │
│ │ Create checkout session │
│ ├──────────────────────────────>│
│ │ │
│ │ {url, sessionId} │
│ │<──────────────────────────────┤
│ │ │
│ {checkoutUrl, sessionId} │ │
│<──────────────────────────┤ │
│ │ │
│ Redirect to checkoutUrl │ │
├───────────────────────────────────────────────────────────>
│ │ │
│ │ Webhook: payment.success │
│ │<──────────────────────────────┤
│ │ Update subscription │
│ │ │
Payment Factory¶
src/lib/payment/PaymentFactory.ts
├── StripeService — fully implemented (checkout, webhooks, subscriptions)
└── LemonSqueezy — SDK setup exists, but NOT wired into factory (placeholder)
Webhook Processing¶
// Backend: POST /webhooks/stripe (root level, not under /api/v1)
app.post("/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["stripe-signature"];
const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
switch (event.type) {
case "checkout.session.completed":
await handleCheckoutComplete(event.data.object);
break;
case "customer.subscription.updated":
await handleSubscriptionUpdate(event.data.object);
break;
}
res.json({ received: true });
}
);
Known Issue: The webhook secret has a hardcoded fallback value when
STRIPE_WEBHOOK_SECRETis not set, which is a security risk in production.
Frontend Stripe Keys¶
Email Integration¶
Email Factory¶
src/lib/email/EmailFactory.ts
├── NodeMailerService — SMTP-based (default)
├── ResendService — Resend API
└── BrevoService — Brevo (Sendinblue) API
Event-Driven Architecture¶
User Action
│
▼
EventEmitter.emit("user.registered", { user, token })
│
▼
Event Handler (src/subscribers/)
│
▼
EmailFactory.create(provider) -> NodeMailerService | ResendService | BrevoService
│
▼
Send email using hardcoded templates from src/template/ (5 template files)
Known Issue: The
EmailTemplatedatabase entity (with visual builder in the admin UI) is completely disconnected from the email sending pipeline. All emails are sent using hardcoded templates insrc/template/. The DB templates and visual builder are a dead end.
CORS Configuration¶
Backend CORS Setup¶
// app.ts
import cors from "cors";
import { getCorsOrigins } from "./shared/getCorsOrigins";
app.use(cors({
origin: getCorsOrigins(), // Parses ALLOWED_ORIGINS env var (JSON array)
credentials: true, // Allow cookies
}));
Default CORS origins (when ALLOWED_ORIGINS is not set):
Frontend Credentials¶
// axiosInstance.js
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
withCredentials: true, // Send cookies with requests
});
Error Handling¶
Backend Error Response¶
// ApiError class
throw new ApiError(httpStatus.NOT_FOUND, "User not found");
// Global error handler response shape
{
"success": false,
"message": "User not found",
"error": {
"path": "/api/v1/users/123",
"message": "User not found"
}
}
Frontend Error Handling¶
// In React Query mutation
const createUser = useCreateUser();
const handleSubmit = async (data) => {
try {
await createUser.mutateAsync(data);
toast.success("User created successfully");
} catch (error) {
toast.error(error.response?.data?.message || "Failed to create user");
}
};
Known Integration Issues¶
| # | Severity | Issue | Impact |
|---|---|---|---|
| 1 | Critical | JWT stored in non-httpOnly cookie | XSS can steal the auth token directly via document.cookie. Frontend reads it with getCookie(), so httpOnly cannot be added without rearchitecting auth. |
| 2 | High | 403 triggers full logout (same as 401) | Users lose their session when they merely lack a specific permission, rather than only on actual auth failures. |
| 3 | High | Triple-cache desync | React Query, Redux, and Next.js fetch cache all store overlapping data (e.g., user profile, settings) with no coordination. Mutations may update one cache but leave others stale. |
| 4 | Medium | 4 legacy hooks double-prefix API URL | Some older hooks prepend NEXT_PUBLIC_API_URL to paths that already include the base URL, resulting in URLs like .../api/v1/http://localhost:5500/api/v1/.... |
| 5 | Low | useMenu passes wrong param name |
Sends limits instead of limit, causing the backend to ignore the pagination parameter. |
| 6 | Low | LemonSqueezy placeholder | SDK setup exists but factory integration is not implemented — calling it will fail silently or throw. |
Deployment Considerations¶
Production URLs¶
# Frontend (.env.production)
NEXT_PUBLIC_API_URL=https://api.yourdomain.com/api/v1
API_URL=https://api.yourdomain.com/api/v1
STRIPE_PUBLISHABLE_KEY=pk_live_...
# Backend (.env.production)
CLIENT_URL=https://app.yourdomain.com
ALLOWED_ORIGINS=["https://app.yourdomain.com"]
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_live_...
Cookie Settings (Production)¶
Important: The current auth architecture stores the JWT in a JS-accessible cookie — the frontend reads it via
getCookie("token")and sets it viasetCookie(). This meanshttpOnlycannot be used without reworking the auth flow. If migrating to httpOnly cookies, the backend must set the cookie in the response header and the frontend must stop reading the token directly.
// Current architecture (JS-accessible cookie):
setCookie("token", value, {
maxAge: 60 * 60 * 24 * 30, // 30 days (rememberMe) or 7 days
secure: true, // HTTPS only (auto in production)
sameSite: "strict", // Prevent CSRF
path: "/",
});
// Note: httpOnly is intentionally omitted — frontend JS reads this cookie.
// Mitigate XSS risk via CSP headers, input sanitization, and content security policies.
Cache Revalidation in Production¶
When deploying to Vercel or similar platforms, the revalidateTag() server actions work across the CDN edge. The admin UI at /dashboard/settings/revalidate provides manual purge capabilities for all 7 cache tags.
Generated by BMAD Document Project workflow v1.2.0 — Exhaustive scan, 2026-02-12 | Last verified: 2026-03-26