MERN Reference Code — Frontend Layer
The backend doc already covers Express/Mongoose/JWT/[Link]. This fills in the React side
— the pieces that talk to that backend: an auth context, a protected-route wrapper, an Axios
API layer with token refresh, a role-based dashboard, and a couple of reusable
hooks/components.
1. Axios API layer with automatic token refresh
// api/[Link]
import axios from 'axios';
const api = [Link]({
baseURL: [Link].REACT_APP_API_URL,
withCredentials: true, // sends the httpOnly refresh-token cookie
});
// attach access token to every request
[Link]((config) => {
const token = [Link]('accessToken');
if (token) [Link] = `Bearer ${token}`;
return config;
});
// if a request fails with 401, try refreshing the access token once, then retry
let isRefreshing = false;
let queue = [];
[Link](
(res) => res,
async (error) => {
const originalRequest = [Link];
if ([Link]?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// queue requests that arrive while a refresh is already in flight
return new Promise((resolve, reject) => {
[Link]({ resolve, reject });
}).then((token) => {
[Link] = `Bearer ${token}`;
return api(originalRequest);
});
originalRequest._retry = true;
isRefreshing = true;
try {
const { data } = await [Link](
`${[Link].REACT_APP_API_URL}/auth/refresh`,
{},
{ withCredentials: true }
);
[Link]('accessToken', [Link]);
[Link](({ resolve }) => resolve([Link]));
queue = [];
[Link] = `Bearer ${[Link]}`;
return api(originalRequest);
} catch (refreshError) {
[Link](({ reject }) => reject(refreshError));
queue = [];
[Link]('accessToken');
[Link] = '/login';
return [Link](refreshError);
} finally {
isRefreshing = false;
return [Link](error);
);
export default api;
Why the queue? If five API calls fire at once and all get a 401, you don't want five separate
refresh requests racing each other — the first one refreshes, and the other four wait on the
queue and retry with the new token once it resolves.
2. Auth context — global user/role state
// context/[Link]
import { createContext, useContext, useState, useEffect } from 'react';
import api from '../api/client';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// on app load, try to restore session from the refresh cookie
[Link]('/auth/refresh')
.then(({ data }) => {
[Link]('accessToken', [Link]);
return [Link]('/auth/me');
})
.then(({ data }) => setUser(data))
.catch(() => setUser(null))
.finally(() => setLoading(false));
}, []);
async function login(email, password) {
const { data } = await [Link]('/auth/login', { email, password });
[Link]('accessToken', [Link]);
setUser({ role: [Link], email });
function logout() {
[Link]('accessToken');
setUser(null);
[Link]('/auth/logout');
return (
<[Link] value={{ user, loading, login, logout }}>
{children}
</[Link]>
);
export const useAuth = () => useContext(AuthContext);
3. Protected + role-gated routes
// routes/[Link]
import { Navigate, Outlet } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export function ProtectedRoute() {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
return user ? <Outlet /> : <Navigate to="/login" replace />;
export function RoleRoute({ allowedRoles }) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Navigate to="/login" replace />;
if () return <Navigate to="/unauthorized" replace />;
return <Outlet />;
// [Link]
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ProtectedRoute, RoleRoute } from './routes/ProtectedRoute';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route element={<RoleRoute allowedRoles={['admin', 'committee']} />}>
<Route path="/admin" element={<AdminPanel />} />
</Route>
<Route element={<RoleRoute allowedRoles={['maintenance']} />}>
<Route path="/maintenance" element={<MaintenanceQueue />} />
</Route>
</Route>
</Routes>
</BrowserRouter>
);
Nesting RoleRoute inside ProtectedRoute mirrors the backend's two-layer check
(authenticate, then authorize) — a clean thing to point out if an interviewer asks how
frontend and backend auth logic relate.
4. Reusable data-fetching hook
// hooks/[Link]
import { useState, useEffect, useCallback } from 'react';
import api from '../api/client';
export function useFetch(url, deps = []) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await [Link](url);
setData([Link]);
setError(null);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}, [url]);
useEffect(() => {
fetchData();
}, [...deps, fetchData]);
return { data, error, loading, refetch: fetchData };
// Usage
function ComplaintsList() {
const { data: complaints, loading, error, refetch } = useFetch('/complaints');
if (loading) return <Spinner />;
if (error) return <ErrorBanner message="Couldn't load complaints" />;
return (
<ul>
{[Link]((c) => (
<ComplaintCard key={c._id} complaint={c} onUpdate={refetch} />
))}
</ul>
);
5. [Link] client — matching the backend's room pattern
// hooks/[Link]
import { useEffect, useRef } from 'react';
import { io } from '[Link]-client';
export function useSocket(onEvents = {}) {
const socketRef = useRef(null);
useEffect(() => {
const token = [Link]('accessToken');
[Link] = io([Link].REACT_APP_SOCKET_URL, {
auth: { token },
});
[Link](onEvents).forEach(([event, handler]) => {
[Link](event, handler);
});
return () => [Link]();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return socketRef;
// Usage — live-update a complaint's status without polling
function ComplaintDetail({ complaintId }) {
const [status, setStatus] = useState(null);
useSocket({
'complaint:updated': (payload) => {
if ([Link] === complaintId) setStatus([Link]);
},
});
return <StatusBadge status={status} />;
6. Reusable form component (the "component library" line on your resume)
// components/[Link]
export function FormField({ label, error, children }) {
return (
<div className="form-field">
<label>{label}</label>
{children}
{error && <span className="form-error">{error}</span>}
</div>
);
// components/[Link]
export function Button({ variant = 'primary', loading, children, ...props }) {
return (
<button className={`btn btn-${variant}`} disabled={loading} {...props}>
{loading ? <Spinner size="sm" /> : children}
</button>
);
// Usage — a complaint submission form built from the shared pieces
function NewComplaintForm({ onSuccess }) {
const [form, setForm] = useState({ category: 'noise', description: '' });
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
async function handleSubmit(e) {
[Link]();
setSubmitting(true);
try {
await [Link]('/complaints', form);
onSuccess();
} catch (err) {
setError([Link]?.data?.message || 'Something went wrong');
} finally {
setSubmitting(false);
return (
<form onSubmit={handleSubmit}>
<FormField label="Category">
<select
value={[Link]}
onChange={(e) => setForm({ ...form, category: [Link] })}
>
<option value="noise">Noise</option>
<option value="maintenance">Maintenance</option>
<option value="security">Security</option>
</select>
</FormField>
<FormField label="Description" error={error}>
<textarea
value={[Link]}
onChange={(e) => setForm({ ...form, description: [Link] })}
/>
</FormField>
<Button type="submit" loading={submitting}>Submit Complaint</Button>
</form>
);
}
Quick talking points if asked about the frontend architecture
• State management: Context API for auth (global, low-frequency updates) rather than
Redux — a reasonable call for an app this size; if pushed on "why not Redux," the
honest answer is auth state doesn't need the ceremony of actions/reducers when a
single context covers it.
• Token refresh race condition: the isRefreshing + queue pattern in the Axios
interceptor is the single most interview-worthy piece of frontend code here — be
ready to explain it without the code in front of you.
• Route nesting mirrors backend middleware order: ProtectedRoute (authentication)
wraps RoleRoute (authorization), same two-step check as the backend's authenticate
→ authorize middleware chain.