# 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 (
Product Library
{/* Client Component */}
{/* Client Component for interactivity */}
);
}
```
### 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 ;
}
```
### 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 ;
if (error) return ;
return 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 (
<>
>
);
}
```
## Loading & Error States
### Use `loading.tsx` for route-level loading
```typescript
// app/(dashboard)/products/loading.tsx
export default function Loading() {
return ;
}
```
### 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 (
Something went wrong
{error.message}
);
}
```
### Use `` for granular loading within a page
```typescript
import { Suspense } from 'react';
export default function PantryPage() {
return (