Routing and Navigation
Routing and Navigation
Section titled “Routing and Navigation”Problem and Rationale
Section titled “Problem and Rationale”RednGift requires organized navigation between:
- Public pages: Landing, invitation acceptance
- Authentication pages: Login, registration, magic link confirmation
- Protected pages: Groups, wishlists, chat, settings
Context and Constraints
Section titled “Context and Constraints”- SEO: Static pages should be server-rendered for search engines
- Performance: Code-split routes to reduce initial bundle size
- Type safety: Prevent typos in navigation paths
- Access control: Enforce authentication before rendering protected pages
- Layout reuse: Share navbar/footer across route groups
Why Next.js App Router?
Section titled “Why Next.js App Router?”- File-based routing: Folder structure defines URL paths automatically
- Layouts: Shared UI across route hierarchies
- Server components: Reduce client JavaScript by default
- Streaming: Progressive page rendering with React Suspense
- Metadata API: Type-safe SEO tags and OpenGraph
Why Route Guards?
Section titled “Why Route Guards?”- Security: Prevent unauthorized access to user data
- UX: Auto-redirect unauthenticated users to login
- Loading states: Show skeleton while checking auth status
Design
Section titled “Design”Route Structure
Section titled “Route Structure”src/app/├── layout.tsx # Root layout (providers, theme)├── page.tsx # Public home page (/)├── not-found.tsx # 404 page├── loading.tsx # Global loading fallback├── auth/│ ├── layout.tsx # Auth layout (GuestGuard, centered card)│ ├── login/│ │ └── page.tsx # /auth/login│ ├── register/│ │ └── page.tsx # /auth/register│ ├── confirm/│ │ └── page.tsx # /auth/confirm (email confirmation)│ └── magic/│ └── [token]/│ └── page.tsx # /auth/magic/:token (passwordless login)├── (home)/ # Route group (doesn't affect URL)│ ├── layout.tsx # Authenticated layout (AuthGuard, navbar)│ ├── page.tsx # /home (dashboard)│ ├── group/│ │ ├── create/│ │ │ └── page.tsx # /group/create│ │ ├── [id]/│ │ │ ├── page.tsx # /group/:id│ │ │ ├── edit/│ │ │ │ └── page.tsx # /group/:id/edit│ │ │ ├── invite/│ │ │ │ ├── page.tsx # /group/:id/invite│ │ │ │ └── batch/│ │ │ │ └── page.tsx # /group/:id/invite/batch│ │ │ └── wishlists/│ │ │ ├── page.tsx # /group/:id/wishlists│ │ │ └── [wishlistId]/│ │ │ └── page.tsx # /group/:id/wishlists/:wishlistId│ ├── inbox/│ │ ├── page.tsx # /inbox│ │ └── [id]/│ │ └── page.tsx # /inbox/:id│ ├── items/│ │ └── page.tsx # /items (trending items)│ ├── wishlist/│ │ ├── create/│ │ │ └── page.tsx # /wishlist/create│ │ └── [id]/│ │ └── edit/│ │ ├── page.tsx # /wishlist/:id/edit│ │ └── items/│ │ └── page.tsx # /wishlist/:id/edit/items│ └── settings/│ └── profile/│ ├── page.tsx # /settings/profile│ └── wishlists/│ └── page.tsx # /settings/profile/wishlists├── invite/│ ├── layout.tsx # Invitation layout (minimal UI)│ └── [slug]/│ └── page.tsx # /invite/:slug (public group invite)└── terminos-y-condiciones/ └── page.tsx # /terminos-y-condiciones (legal)Route groups (home): Parentheses prevent folder name from appearing in URL.
Path Constants
Section titled “Path Constants”const ROOTS = { AUTH: '/auth', HOME: '/',};
export const paths = { auth: { confirm: `${ROOTS.AUTH}/confirm`, login: `${ROOTS.AUTH}/login`, register: `${ROOTS.AUTH}/register`, magic: (magicToken: string) => `${ROOTS.AUTH}/magic/${magicToken}`, }, home: { main: ROOTS.HOME, group: { main: (id: number | string) => `${ROOTS.HOME}group/${id}`, create: `${ROOTS.HOME}group/create`, edit: (id: number | string) => `${ROOTS.HOME}group/${id}/edit`, wishlists: { main: (id: number | string) => `${ROOTS.HOME}group/${id}/wishlists`, wishlist: (idGroup: number | string, idWishlist: number | string) => `${ROOTS.HOME}group/${idGroup}/wishlists/${idWishlist}`, }, invite: { main: (id: number | string, back?: string) => `${ROOTS.HOME}group/${id}/invite${back === 'true' ? '?back=1' : '?back=0'}`, batch: (id: number | string, group?: string) => `${ROOTS.HOME}group/${id}/invite/batch${group === 'true' ? '?group=1' : '?group=0'}`, }, }, inbox: { main: `${ROOTS.HOME}inbox`, id: (id: number | string) => `${ROOTS.HOME}inbox/${id}`, }, wishlist: { create: `${ROOTS.HOME}wishlist/create`, edit: { main: (id: number | string) => `${ROOTS.HOME}wishlist/${id}/edit?wishlist=${id}`, editItems: (id: number | string) => `${ROOTS.HOME}wishlist/${id}/edit/items`, }, }, settings: { profile: { main: `${ROOTS.HOME}settings/profile`, wishlists: `${ROOTS.HOME}settings/profile/wishlists`, }, }, },};Benefits:
- Type-safe navigation: TypeScript catches invalid paths at compile time
- Single source of truth: Change URL structure in one place
- Auto-complete: IDE suggests available paths
Route Guards
Section titled “Route Guards”AuthGuard (Protected Routes)
Section titled “AuthGuard (Protected Routes)”// src/auth/guard/auth-guard.tsx (simplified)'use client';
import { useEffect } from 'react';import { useRouter } from 'next/navigation';import { useAuthContext } from 'src/auth/hooks';import LoadingScreen from 'src/components/loading-screen';
export function AuthGuard({ children }: Props) { const { authenticated, loading } = useAuthContext(); const router = useRouter();
useEffect(() => { if (!loading && !authenticated) { router.replace('/auth/login'); } }, [authenticated, loading, router]);
if (loading) { return <LoadingScreen />; }
if (!authenticated) { return null; // Will redirect in useEffect }
return <>{children}</>;}Usage:
// src/app/(home)/layout.tsxexport default function Layout({ children }: Props) { return ( <AuthGuard> <WebSocketProvider> <HomeLayout>{children}</HomeLayout> </WebSocketProvider> </AuthGuard> );}GuestGuard (Redirect Authenticated Users)
Section titled “GuestGuard (Redirect Authenticated Users)”// src/auth/guard/guest-guard.tsx (simplified)export function GuestGuard({ children }: Props) { const { authenticated, loading } = useAuthContext(); const router = useRouter();
useEffect(() => { if (!loading && authenticated) { router.replace('/'); // Redirect to home if already logged in } }, [authenticated, loading, router]);
if (loading || authenticated) { return <LoadingScreen />; }
return <>{children}</>;}Usage: Wrap /auth/login and /auth/register layouts.
RoleBasedGuard (Future)
Section titled “RoleBasedGuard (Future)”export function RoleBasedGuard({ children, allowedRoles }: Props) { const { user } = useAuthContext();
if (!allowedRoles.includes(user?.role)) { return <ForbiddenPage />; }
return <>{children}</>;}Assumption: Role-based access control not currently implemented.
Lifecycle and Data Flow
Section titled “Lifecycle and Data Flow”1. Navigation Flow (User Clicks Link)
Section titled “1. Navigation Flow (User Clicks Link)”User clicks <Link href={paths.home.group.main(5)}> │ ▼Next.js Router intercepts │ ▼Pre-fetch page chunk (if in viewport) │ ▼Navigate to /group/5 │ ▼Render layout hierarchy: layout.tsx (root) └─ (home)/layout.tsx └─ group/[id]/page.tsx │ ▼Execute guards (AuthGuard) │ ├─ Authenticated ─────────────┐ │ │ │ ▼ │ Render page │ └─ Unauthenticated ───────────┐ │ ▼ Redirect to /auth/login2. Deep Link Access (User Types URL Directly)
Section titled “2. Deep Link Access (User Types URL Directly)”User visits /group/5/wishlists │ ▼Next.js loads page bundle │ ▼AuthProvider initializes │ ├─ Token valid ──────────────┐ │ │ │ ▼ │ Set user in context │ │ │ ▼ │ AuthGuard passes │ │ │ ▼ │ Render /group/5/wishlists │ └─ No token ─────────────────┐ │ ▼ AuthGuard redirects to /auth/login │ ▼ Store return URL: /group/5/wishlists │ ▼ After login, redirect back3. Programmatic Navigation
Section titled “3. Programmatic Navigation”// Component codeconst router = useRouter();
const handleCreateGroup = async () => { const group = await groupsHandlers.createNewGroup(data); router.push(paths.home.group.main(group.id));};Methods:
router.push(path): Navigate with back button supportrouter.replace(path): Navigate without history entryrouter.back(): Go to previous pagerouter.refresh(): Re-fetch server data without full reload
Implementation Notes
Section titled “Implementation Notes”Patterns
Section titled “Patterns”✅ DO:
- Use
pathsobject for all navigation:<Link href={paths.home.inbox.main} /> - Use dynamic segments
[id]for variable routes - Wrap layouts with guards, not individual pages
- Pre-fetch critical routes:
<Link prefetch href={...} />
❌ DON’T:
- Don’t hardcode URLs:
<Link href="/group/5" />(usepaths.home.group.main(5)) - Don’t nest
AuthGuardmultiple times (apply once at layout level) - Don’t use
window.location.hreffor client-side navigation (loses SPA behavior)
Query Parameters
Section titled “Query Parameters”// Reading query paramsimport { useSearchParams } from 'next/navigation';
const searchParams = useSearchParams();const back = searchParams.get('back'); // /group/5/invite?back=1Setting query params:
const router = useRouter();router.push(`${paths.home.group.invite.main(id)}?back=1`);Dynamic Metadata (SEO)
Section titled “Dynamic Metadata (SEO)”// src/app/(home)/group/[id]/page.tsximport { Metadata } from 'next';
export async function generateMetadata({ params }: Props): Promise<Metadata> { const group = await groupsHandlers.getGroupById(params.id);
return { title: `${group.name} - RednGift`, description: `Join ${group.name} for Secret Santa gift exchange`, openGraph: { title: group.name, images: [group.image], }, };}Error Handling
Section titled “Error Handling”- 404 pages: Create
not-found.tsxat any level - Error boundaries: Create
error.tsxat any level
// src/app/(home)/group/[id]/error.tsx'use client';
export default function Error({ error, reset }: Props) { return ( <div> <h2>Failed to load group</h2> <button onClick={reset}>Try again</button> </div> );}Performance Considerations
Section titled “Performance Considerations”- Automatic code splitting: Each route is a separate chunk
- Pre-fetching: Links in viewport are pre-fetched on hover
- Parallel routes: Fetch multiple route segments simultaneously
- Streaming: Use
loading.tsxto show skeleton while data loads
// src/app/(home)/group/[id]/loading.tsxexport default function Loading() { return <Skeleton variant="rectangular" height={400} />;}Testing Strategy
Section titled “Testing Strategy”Unit Tests (Guards)
Section titled “Unit Tests (Guards)”import { renderHook } from '@testing-library/react';import { AuthGuard } from './auth-guard';
it('should redirect unauthenticated users', () => { jest.spyOn(useAuthContext, 'useAuthContext').mockReturnValue({ authenticated: false, loading: false, });
const routerPush = jest.fn(); jest.spyOn(useRouter, 'useRouter').mockReturnValue({ push: routerPush });
renderHook(() => <AuthGuard><div /></AuthGuard>);
expect(routerPush).toHaveBeenCalledWith('/auth/login');});Integration Tests (Navigation)
Section titled “Integration Tests (Navigation)”import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';
it('should navigate to group page on link click', async () => { render(<GroupCard groupId={5} />);
const link = screen.getByRole('link', { name: /view group/i }); await userEvent.click(link);
expect(window.location.pathname).toBe('/group/5');});E2E Tests
Section titled “E2E Tests”- Navigate to protected route when logged out → redirects to login
- Log in → redirects back to original URL
- Navigate through multi-level routes (group → wishlists → items)
Extending This Concept
Section titled “Extending This Concept”Adding Admin Routes
Section titled “Adding Admin Routes”export const paths = { admin: { dashboard: '/admin/dashboard', users: '/admin/users', reports: '/admin/reports', },};
// src/app/admin/layout.tsximport { RoleBasedGuard } from 'src/auth/guard';
export default function AdminLayout({ children }: Props) { return ( <AuthGuard> <RoleBasedGuard allowedRoles={['admin']}> {children} </RoleBasedGuard> </AuthGuard> );}Adding Multi-language Routes
Section titled “Adding Multi-language Routes”export async function generateStaticParams() { return [{ lang: 'en' }, { lang: 'es' }];}
export default function LocaleLayout({ children, params }: Props) { return ( <IntlProvider locale={params.lang}> {children} </IntlProvider> );}URLs become /en/group/5 and /es/group/5.
Adding Route Analytics
Section titled “Adding Route Analytics”Track page views:
'use client';
import { usePathname, useSearchParams } from 'next/navigation';import { useEffect } from 'react';
export default function RootLayout({ children }: Props) { const pathname = usePathname(); const searchParams = useSearchParams();
useEffect(() => { const url = `${pathname}?${searchParams}`; gtag('config', GA_TRACKING_ID, { page_path: url }); }, [pathname, searchParams]);
return children;}Further Reading
Section titled “Further Reading”- Authentication - Route guards and auth integration
- Secret Santa Groups - Group route hierarchy
- API Layer - Fetching data in route components
- Navigation Components - Navbar and breadcrumbs