# Next.js Best Practices — MeshiTrack Web

> Instruction file for developing the Next.js frontend (`packages/web`).
> Uses **App Router** (not Pages Router), **TypeScript**, and **Tailwind CSS**.

## Project Structure

```
packages/web/src/
├── app/                          # App Router — file-based routing
│   ├── layout.tsx                # Root layout (html, body, providers)
│   ├── page.tsx                  # Dashboard / home page
│   ├── loading.tsx               # Root loading state
│   ├── error.tsx                 # Root error boundary
│   ├── not-found.tsx             # 404 page
│   ├── (auth)/                   # Route group: unauthenticated pages
│   │   ├── login/page.tsx
│   │   └── layout.tsx
│   ├── (dashboard)/              # Route group: authenticated pages
│   │   ├── layout.tsx            # Sidebar + topbar layout
│   │   ├── products/
│   │   │   ├── page.tsx          # Product list
│   │   │   ├── [id]/page.tsx     # Product detail
│   │   │   └── loading.tsx
│   │   ├── recipes/
│   │   ├── pantry/
│   │   ├── meal-plans/
│   │   ├── shopping-lists/
│   │   └── settings/
│   └── api/                      # Route Handlers (if needed for BFF patterns)
├── components/                   # Shared React components
│   ├── ui/                       # Generic UI components (Button, Modal, Card, etc.)
│   ├── forms/                    # Form components
│   ├── layout/                   # Navigation, Sidebar, TopBar
│   └── features/                 # Feature-specific composed components
│       ├── products/
│       ├── recipes/
│       ├── pantry/
│       └── shopping/
├── hooks/                        # Custom React hooks
├── services/                     # API client layer
│   ├── api-client.ts             # Configured fetch/axios wrapper
│   ├── products.service.ts
│   ├── recipes.service.ts
│   └── ...
├── lib/                          # Utility functions, constants
├── styles/                       # Global styles, Tailwind config
└── types/                        # Frontend-specific types (import shared types from @meshitrack/shared)
```

## Server vs Client Components

### Default to Server Components

Every component in the App Router is a **Server Component** by default. Keep it that way unless the component needs:

- Browser APIs (`window`, `document`, `localStorage`)
- React hooks (`useState`, `useEffect`, `useRef`, etc.)
- Event handlers (`onClick`, `onChange`, etc.)
- Browser-only libraries

### Mark Client Components explicitly with `'use client'`

```typescript
'use client';

import { useState } from 'react';

export function ProductSearchBar({ onSearch }: { onSearch: (q: string) => void }) {
  const [query, setQuery] = useState('');
  // ...interactive UI
}
```

### Composition pattern: Server parent, Client children

```typescript
// app/(dashboard)/products/page.tsx — Server Component
import { ProductSearchBar } from '@/components/features/products/ProductSearchBar';
import { ProductList } from '@/components/features/products/ProductList';

export default async function ProductsPage() {
  // Can fetch data directly on the server
  const initialProducts = await fetchProducts();

  return (
    <div>
      <h1>Product Library</h1>
      <ProductSearchBar />                           {/* Client Component */}
      <ProductList initialData={initialProducts} />  {/* Client Component for interactivity */}
    </div>
  );
}
```

### Never import server-only code in Client Components

If a utility should only run on the server, use the `server-only` package:

```typescript
import 'server-only';

export async function getServerConfig() {
  // This will error if accidentally imported from a Client Component
}
```

## Data Fetching

### In Server Components: fetch directly

```typescript
// app/(dashboard)/products/page.tsx
export default async function ProductsPage() {
  const res = await fetch(`${process.env.API_URL}/api/v1/products`, {
    headers: { Authorization: `Bearer ${await getToken()}` },
    cache: 'no-store',  // Always fresh for user-specific data
  });
  const data = await res.json();
  return <ProductGrid products={data.data} />;
}
```

### In Client Components: use SWR or React Query

We recommend **SWR** for most data fetching in Client Components:

```typescript
'use client';

import useSWR from 'swr';
import { apiClient } from '@/services/api-client';

export function PantryDashboard() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/v1/pantry?sort=-freshnessEstimate.daysRemaining',
    apiClient.get,
  );

  if (isLoading) return <PantrySkeleton />;
  if (error) return <ErrorDisplay error={error} />;

  return <PantryGrid items={data.data} onUpdate={() => mutate()} />;
}
```

### Parallel data fetching

When a page needs multiple independent data sources, fetch in parallel:

```typescript
export default async function DashboardPage() {
  const [pantryData, mealPlanData, shoppingData] = await Promise.all([
    fetchExpiringSoon(),
    fetchCurrentMealPlan(),
    fetchActiveShoppingLists(),
  ]);

  return (
    <>
      <ExpiringItems items={pantryData} />
      <CurrentMealPlan plan={mealPlanData} />
      <ActiveShoppingLists lists={shoppingData} />
    </>
  );
}
```

## Loading & Error States

### Use `loading.tsx` for route-level loading

```typescript
// app/(dashboard)/products/loading.tsx
export default function Loading() {
  return <ProductGridSkeleton />;
}
```

### Use `error.tsx` for route-level error boundaries

```typescript
// app/(dashboard)/products/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}
```

### Use `<Suspense>` for granular loading within a page

```typescript
import { Suspense } from 'react';

export default function PantryPage() {
  return (
    <div>
      <h1>Pantry</h1>
      <Suspense fallback={<FreshnessAlertsSkeleton />}>
        <FreshnessAlerts />
      </Suspense>
      <Suspense fallback={<PantryGridSkeleton />}>
        <PantryGrid />
      </Suspense>
    </div>
  );
}
```

## API Client Layer

### Centralized API client with auth

```typescript
// services/api-client.ts
import { getSession } from '@/lib/auth';

const BASE_URL = process.env.NEXT_PUBLIC_API_URL;

class ApiClient {
  private async getHeaders(): Promise<HeadersInit> {
    const session = await getSession();
    return {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${session?.accessToken}`,
    };
  }

  async get<T>(url: string): Promise<T> {
    const res = await fetch(`${BASE_URL}${url}`, {
      headers: await this.getHeaders(),
    });
    if (!res.ok) throw await this.handleError(res);
    return res.json();
  }

  async post<T>(url: string, body: unknown): Promise<T> {
    const res = await fetch(`${BASE_URL}${url}`, {
      method: 'POST',
      headers: await this.getHeaders(),
      body: JSON.stringify(body),
    });
    if (!res.ok) throw await this.handleError(res);
    return res.json();
  }

  // ... patch, delete, upload methods
}

export const apiClient = new ApiClient();
```

### Feature-specific service files

```typescript
// services/products.service.ts
import { apiClient } from './api-client';
import type { Product, PaginatedResponse, CreateProductDto } from '@meshitrack/shared';

export const productsService = {
  list: (params?: Record<string, string>) =>
    apiClient.get<PaginatedResponse<Product>>(`/products?${new URLSearchParams(params)}`),

  getById: (id: string) => apiClient.get<Product>(`/products/${id}`),

  create: (data: CreateProductDto) => apiClient.post<Product>('/products', data),

  update: (id: string, data: Partial<CreateProductDto>) =>
    apiClient.patch<Product>(`/products/${id}`, data),
};
```

## Layouts & Navigation

### Root layout: providers and global UI

```typescript
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AuthProvider>
          <ThemeProvider>
            {children}
          </ThemeProvider>
        </AuthProvider>
      </body>
    </html>
  );
}
```

### Dashboard layout: sidebar + topbar

```typescript
// app/(dashboard)/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen">
      <Sidebar />
      <div className="flex-1 flex flex-col">
        <TopBar />
        <main className="flex-1 overflow-auto p-6">
          {children}
        </main>
      </div>
    </div>
  );
}
```

### Use route groups `(folder)` for shared layouts

Route groups (parenthesized folder names) don't affect the URL:

- `(auth)` — login, register pages with minimal layout
- `(dashboard)` — all authenticated pages with full navigation

## Forms

### Use controlled forms with validation

```typescript
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { CreateProductSchema } from '@meshitrack/shared';

export function ProductForm({ onSubmit }: { onSubmit: (data: CreateProductInput) => void }) {
  const form = useForm({
    resolver: zodResolver(CreateProductSchema),
    defaultValues: { name: '', category: '', servingSize: 0, ... },
  });

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <input {...form.register('name')} />
      {form.formState.errors.name && <span>{form.formState.errors.name.message}</span>}
      {/* ... */}
    </form>
  );
}
```

### Optimistic updates for real-time feel

```typescript
const { trigger, isMutating } = useSWRMutation('/api/v1/pantry/item/transition', apiClient.post);

async function handleConsume(itemId: string) {
  // Optimistically update local data
  mutate(
    (currentData) => ({
      ...currentData,
      data: currentData.data.map((item) =>
        item.id === itemId ? { ...item, status: 'consumed' } : item,
      ),
    }),
    false,
  );

  // Then send to server
  await trigger({ itemId, status: 'consumed' });
}
```

## Shared Types from `@meshitrack/shared`

### Import types from the shared package

```typescript
import type { Product, NutritionInfo, ProductCategory } from '@meshitrack/shared';
import { ServingUnit, ProductSource } from '@meshitrack/shared';
```

### Never duplicate types in the web package

If a type is used in both API and web, it **must** live in `packages/shared`. The web package only defines frontend-specific types (e.g., UI state, component props).

## Authentication (Keycloak)

### Use `next-auth` or `keycloak-js` for OIDC

For App Router, `next-auth` v5 with the Keycloak provider is recommended:

```typescript
// lib/auth.ts
import NextAuth from 'next-auth';
import Keycloak from 'next-auth/providers/keycloak';

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Keycloak({
      clientId: process.env.KEYCLOAK_CLIENT_ID!,
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
      issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken as string;
      return session;
    },
  },
});
```

### Protect routes with middleware

```typescript
// proxy.ts (Next.js 16 renamed middleware.ts → proxy.ts)
export { auth as proxy } from '@/lib/auth';

export const config = {
  matcher: ['/(dashboard)/:path*'], // Protect all dashboard routes
};
```

## Performance Tips

- **Use `next/image`** for all images — automatic optimization, lazy loading, responsive sizing
- **Use `next/link`** for all internal navigation — prefetching, client-side transitions
- **Lazy load heavy components** with `dynamic()`:
  ```typescript
  import dynamic from 'next/dynamic';
  const PriceChart = dynamic(() => import('@/components/features/prices/PriceChart'), {
    loading: () => <ChartSkeleton />,
  });
  ```
- **Keep Client Components as small as possible** — push `'use client'` boundary as far down the tree as you can
- **Use `React.memo`** for list items that render frequently (e.g., pantry items, shopping items)

## Testing

- **Component tests**: React Testing Library

  ```typescript
  import { render, screen } from '@testing-library/react';
  import { ProductCard } from '@/components/features/products/ProductCard';

  test('displays product name and calories', () => {
    render(<ProductCard product={mockProduct} />);
    expect(screen.getByText('Chicken Breast')).toBeInTheDocument();
    expect(screen.getByText('165 kcal')).toBeInTheDocument();
  });
  ```

- **E2E tests**: Playwright for critical flows
- **Mock API calls** in tests using MSW (Mock Service Worker)
