2. API boundaries and caching strategy
I would organize API access around business domains: subscriptions, devices, alerts and billing. Components should consume domain-specific hooks rather than call endpoints directly, which keeps transport details outside the presentation layer.
Cache policy should reflect the volatility and sensitivity of each resource. Subscription details can tolerate a longer stale time, while security alerts and device status require more frequent refreshes or push-based updates. Billing information needs explicit invalidation after payment-related actions.
Query keys should include the authenticated user and relevant parameters, such as
['devices', userId]. After a mutation, I would update or invalidate only affected queries instead of refetching the entire dashboard.For security-sensitive data, I would prefer in-memory caching over persistent browser storage and clear query caches on logout or account switching. I would also prevent sensitive responses from being cached by shared infrastructure using appropriate HTTP cache headers.
3. Authentication, token storage and session expiry
For a security-focused product, I would prefer a backend-for-frontend or server-managed session, using
HttpOnly,Securecookies and an appropriateSameSitesetting. JavaScript should not have access to long-lived refresh tokens.Because cookies are attached automatically, I would also implement CSRF protection where necessary.
HttpOnlyreduces token theft through JavaScript, but it does not prevent XSS itself, so CSP, output escaping and dependency hygiene remain necessary.When an API request returns
401, I would trigger a single shared refresh operation, queue or await other failed requests, and retry them once after refresh succeeds. If refresh fails, I would clear sensitive cached data and redirect to login.Across browser tabs, I would synchronize authentication changes using
BroadcastChannel. Preventing simultaneous refresh attempts across tabs requires an actual coordination mechanism, such as Web Locks, or server-side refresh handling—not just broadcasting messages.Authentication proves who the user is; authorization determines what the user can access. The frontend can hide unauthorized actions, but the backend must enforce permissions.
4. Rendering choice: CSR, SSR or hybrid
I would use hybrid rendering. Public marketing pages benefit from SSR or static generation because SEO and first-load performance matter. The authenticated dashboard benefits from server-rendering its initial shell and essential data when that improves perceived performance, followed by client-side interactivity and targeted refetching.
Frequently changing sections, such as device status or security alerts, should update on the client using polling or push-based communication. Server rendering every update would add unnecessary load without improving the experience.
Sensitive authenticated content must never be accidentally cached or shared between users. I would explicitly control caching and make sure user-specific responses are not stored in public CDN caches.
I would choose rendering boundaries based on product requirements, not ideology: SEO needs, personalization, latency, security, infrastructure cost and interaction patterns.
Follow-up: “When would you choose pure CSR for the dashboard?”
When SEO is irrelevant, the application is highly interactive, most data changes after login, and SSR adds operational complexity without measurable gains. I would verify that decision against metrics such as initial load time, interaction responsiveness and user experience on slower devices.
5. Error handling, retries and degraded networks
I would classify failures before deciding how the UI responds.
Network failures, timeouts,
429responses and temporary server errors may warrant retries with exponential backoff and jitter. Validation errors, permission failures and most other4xxresponses should not be retried automatically.Dashboard sections should fail independently. If billing fails but device data loads, the user should still see and manage their devices. React error boundaries handle rendering failures; query-level error states handle request failures.
For degraded networks, I would preserve previously loaded data and clearly mark it as stale. Important actions require explicit pending, success and failure states so users know whether a request completed.
Retrying mutations requires more care than retrying reads. Revoking a device or creating a payment should use idempotency keys where appropriate to avoid repeating side effects.
Follow-up: “How do you avoid showing stale security information?”
I would display when the data was last refreshed and distinguish cached information from confirmed current state. For critical actions, I would verify against the server immediately before execution and avoid presenting stale security status as authoritative.
6. Preventing sensitive data leaks
I would first identify sensitive data: access tokens, personal information, billing details, device identifiers, IP addresses and security events. Then I would minimize what reaches the browser in the first place.
Authentication tokens should not be stored in
localStoragewhen a secure server-managed session is practical. Sensitive application data should stay in memory rather than persist in browser storage or service-worker caches.Logs, analytics and error-monitoring tools should use allowlists and redaction rules. I would avoid sending raw request payloads, authorization headers, full URLs with sensitive query parameters or user-identifying metadata to third-party tools.
At the transport and browser level, I would enforce HTTPS, configure CSP, avoid unsafe HTML injection, and control response caching with appropriate headers. Private user data should never be stored in publicly shared caches.
Finally, I would treat frontend protections as defense in depth: the backend must enforce authorization, redact responses appropriately and keep audit logs for sensitive actions.
Follow-up: “What is dangerous about putting tokens in URL query parameters?”
URLs can appear in browser history, server logs, monitoring platforms and sometimes referrer headers. Secrets belong in secure cookies or authorization headers, not query strings.
7. Testing, observability and gradual rollout
I would test at multiple levels. Unit tests cover pure transformations and permission logic. Integration tests verify dashboard behavior against mocked API responses, including expired sessions, partial failures and optimistic-update rollback. End-to-end tests cover critical journeys such as logging in, viewing subscriptions and revoking devices.
I would prioritize contract testing where frontend and backend teams evolve independently. That reduces the chance of shipping a dashboard that compiles successfully but breaks because an API response changed.
For observability, I would track Core Web Vitals, JavaScript errors, failed requests and business outcomes such as device-revocation success rate. Metrics should be segmented by browser, region, release version and device capability, while avoiding sensitive user data.
New functionality should launch behind feature flags, initially to internal users or a small percentage of customers. I would monitor technical and business metrics, expand gradually and maintain a clear rollback or kill-switch path.
A feature is not finished when the code ships. It is finished when production metrics confirm that it works reliably and does not harm users.
Follow-up: “What would trigger an immediate rollback?”
Any increase in authentication failures, cross-account data exposure, sensitive-data leakage, failed security actions or a significant regression in critical user journeys. For a cybersecurity company, confidentiality failures override feature-delivery targets.
What happens when two tabs refresh an expired token simultaneously?
Without coordination, both tabs receive 401 responses and attempt to refresh the session. If refresh-token rotation is enabled, the first request may invalidate the token before the second request uses it. Depending on server policy, the second request can fail or be interpreted as token reuse, potentially invalidating the session.
A senior-level solution has two layers:
- Within one tab: Deduplicate refresh requests using one shared promise.
- Across tabs: Use the Web Locks API so only one tab performs the refresh. After acquiring the lock, recheck whether another tab already refreshed the session. Use
BroadcastChannelto propagate authentication changes.
let refreshPromise: Promise<void> | null = null;
async function refreshSession(): Promise<void> {
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = navigator.locks
.request("auth-refresh", async () => {
// Another tab might have refreshed while this tab waited.
const session = await fetch("/api/session", {
credentials: "include",
});
if (session.ok) {
return;
}
const response = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
});
if (!response.ok) {
throw new Error("Session refresh failed");
}
})
.finally(() => {
refreshPromise = null;
});
return refreshPromise;
}
async function authenticatedFetch(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
const response = await fetch(input, {
...init,
credentials: "include",
});
if (response.status !== 401) {
return response;
}
await refreshSession();
// Retry once. Never create an infinite refresh loop.
return fetch(input, {
...init,
credentials: "include",
});
}
Interview phrasing:
I would deduplicate refresh operations inside each tab and coordinate across tabs using a browser lock. Once the lock is acquired, I would recheck session validity because another tab may already have refreshed it. Failed refresh clears sensitive state and redirects to login. Each original request retries once.
How does the UI remain consistent after an optimistic update fails?
For example, a user revokes a connected device:
- Cancel active device-list queries so they do not overwrite the optimistic change.
- Snapshot the current device list.
- Remove the device immediately from the cached list.
- Send the revoke request.
- If it fails, restore the snapshot and notify the user.
- Refetch afterward to reconcile with the server.
type Device = {
id: string;
name: string;
};
const devicesKey = ["devices", userId] as const;
const revokeDevice = useMutation({
mutationFn: async (deviceId: string) => {
const response = await fetch(`/api/devices/${deviceId}`, {
method: "DELETE",
credentials: "include",
});
if (!response.ok) {
throw new Error("Could not revoke device");
}
},
onMutate: async (deviceId) => {
await queryClient.cancelQueries({
queryKey: devicesKey,
});
const previousDevices =
queryClient.getQueryData<Device[]>(devicesKey);
queryClient.setQueryData<Device[]>(
devicesKey,
(devices = []) =>
devices.filter((device) => device.id !== deviceId),
);
return { previousDevices };
},
onError: (_error, _deviceId, context) => {
if (context?.previousDevices) {
queryClient.setQueryData(
devicesKey,
context.previousDevices,
);
}
toast.error("Device revocation failed");
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: devicesKey,
});
},
});
Critical security nuance: For revoking a device, displaying immediate removal might falsely imply the device has already lost access. A safer design is to keep the device visible with a “Revoking…” status until the backend confirms success. Optimistic updates are appropriate only when a temporary incorrect state does not create a security or business risk.
Leave a Reply