Fixing AuthGuard 401 Redirects & Adding a Condo Detail Page in a Next.js / NestJS Monorepo
TL;DR: I refactored the Next.js AuthGuard to preserve the original request URL on 401 responses and added a new dynamic route (apps/web/src/app/condominios/complejos/[id]/page.tsx) to display condo KPIs. The changes eliminate infinite logout loops and expose a clean, typed data‑fetching pattern for the new page.
The Problem
Our sales dashboard started returning a 401 Unauthorized page whenever any API call failed authentication. The symptom was a full‑page redirect to /login that lost the original navigation intent, causing users to be “logged out” after a single expired token. In parallel, the product owner requested a one‑page view of each condo’s key performance indicators (KPIs), but we had no route to fetch and render that data.
Error log excerpt:
ERROR [AuthGuard] 401 – Unauthorized – redirecting to /login
INFO Navigation triggered from /condominios/complejos/42
The root cause was two‑fold:
-
AuthGuard was using
router.replace('/login')without persisting the target URL, so after login the user landed on the generic home page. - The Condo KPI page was missing entirely; we only had a placeholder in the design docs.
What I Tried First
1️⃣ Naïve Router Replace
// apps/web/src/app/auth/guard.ts
if (!session) {
router.replace('/login')
}
I assumed a simple replace would be enough. It worked for a fresh login but broke when the token expired mid‑session. The user was forced back to login, and after successful authentication we had no way to navigate back to the original page.
2️⃣ Query‑String Return URL
I attempted to append a returnUrl query param:
router.replace(`/login?returnUrl=${router.asPath}`)
The login page read the param and performed a client‑side router.push(returnUrl). Unfortunately, the login flow runs on the server (NextAuth) and the query param was stripped during the redirect, resulting in a lost URL and a 404 after login.
Both approaches failed to preserve navigation state across server‑side redirects.
The Implementation
1️⃣ Centralized Auth Redirect Helper
I introduced a small utility that stores the intended URL in a HTTP‑only cookie before redirecting. This survives the server‑side login flow and can be read after authentication.
// apps/web/src/lib/authRedirect.ts
import { serialize } from 'cookie';
import { NextRouter } from 'next/router';
export const redirectToLogin = (router: NextRouter, req: any, res: any) => {
const returnUrl = encodeURIComponent(req?.url ?? router.asPath);
const cookie = serialize('returnUrl', returnUrl, {
httpOnly: true,
path: '/',
maxAge: 60 * 5, // 5 minutes
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
});
res.setHeader('Set-Cookie', cookie);
router.replace('/login');
};
AuthGuard now calls this helper:
// apps/web/src/app/auth/guard.ts
import { redirectToLogin } from '@/lib/authRedirect';
export async function AuthGuard({ children }: { children: ReactNode }) {
const router = useRouter();
const { data: session, status } = useSession();
const isLoading = status === 'loading';
if (isLoading) return <Spinner />;
if (!session) {
// `req`/`res` are available via Next.js middleware context
if (typeof window === 'undefined') {
// Server‑side: we have req/res from getServerSideProps
redirectToLogin(router, req, res);
} else {
// Client‑side fallback
router.replace('/login');
}
return null;
}
return <>{children}</>;
}
2️⃣ Post‑Login Return URL Consumption
In the login API route (pages/api/auth/[...nextauth].ts) I added logic to read the cookie and redirect after successful sign‑in.
// pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import { parse } from 'cookie';
export default NextAuth({
// ...providers, callbacks, etc.
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
// normal sign‑in flow
return true;
},
async redirect({ url, baseUrl }) {
// `url` is the default redirect (e.g., /dashboard)
// We inspect the request cookie for a stored returnUrl
const { req } = this;
const cookies = parse(req.headers.cookie ?? '');
const returnUrl = cookies.returnUrl ? decodeURIComponent(cookies.returnUrl) : null;
// Clear the cookie
if (returnUrl) {
req.res?.setHeader('Set-Cookie', 'returnUrl=; Max-Age=0; Path=/');
return `${baseUrl}${returnUrl}`;
}
return baseUrl;
},
},
});
Now a user whose token expires on /condominios/complejos/42 is redirected to login, then automatically returned to the same condo detail page after successful authentication.
3️⃣ Condo Detail Page (page.tsx)
The new dynamic route lives at apps/web/src/app/condominios/complejos/[id]/page.tsx. I leveraged React Server Components (RSC) for data fetching and Zod for runtime validation.
// apps/web/src/app/condominios/complejos/[id]/page.tsx
import { notFound } from 'next/navigation';
import { z } from 'zod';
import { fetchCondoKPIs } from '@/services/condoService';
type Params = { id: string };
const CondoSchema = z.object({
id: z.string().uuid(),
name: z.string(),
kpis: z.object({
occupancy: z.number(),
revenue: z.number(),
units: z.number(),
}),
});
export default async function CondoDetail({ params }: { params: Params }) {
const raw = await fetchCondoKPIs(params.id);
const result = CondoSchema.safeParse(raw);
if (!result.success) {
// Invalid payload – treat as 404
notFound();
}
const { name, kpis } = result.data;
return (
<section className="p-6">
<h1 className="text-2xl font-bold">{name}</h1>
<dl className="mt-4 grid grid-cols-3 gap-4">
<div>
<dt className="text-sm text-gray-500">Occupancy</dt>
<dd className="text-xl">{kpis.occupancy}%</dd>
</div>
<div>
<dt className="text-sm text-gray-500">Revenue</dt>
<dd className="text-xl">${kpis.revenue.toLocaleString()}</dd>
</div>
<div>
<dt className="text-sm text-gray-500">Units</dt>
<dd className="text-xl">{kpis.units}</dd>
</div>
</dl>
</section>
);
}
Service layer (condoService.ts) talks to the NestJS backend:
ts
// apps/web/src/services/condoService.ts
import { API_URL } from '@/config';
export async function fetchCondoKPIs(id: string) {
const
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/content-automation` · 2026-08-29*
\#playadev #buildinpublic
Top comments (0)