# Nextjs Architect

> Guide Next.js App Router architecture decisions — server vs client components, layouts, routing, data fetching, middleware, and project structure. Use this skill whenever the user is building or restructuring a Next.js application, asking about App Router patterns, deciding between server and client components, setting up layouts or route groups, implementing data fetching strategies, or organizing their Next.js project structure. Also use when the user mentions page routing, dynamic routes, parallel routes, intercepting routes, or Next.js middleware.

- Skill: `shiven0504/nextjs-architect` (Agent Skill)
- Install (CLI): `npx skillmds@latest add shiven0504/nextjs-architect`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shiven0504/nextjs-architect/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Shiven0504 (https://skillmd.com/u/shiven0504)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/shiven0504/nextjs-architect

---


# Next.js App Router Architecture

Help users make sound architectural decisions in Next.js App Router projects using Tailwind CSS for styling.

## Server vs Client Components

The most important architectural decision in modern Next.js is understanding the server/client boundary.

### Default to Server Components

Every component is a Server Component unless it opts in with `"use client"`. Keep components on the server when they:

- Fetch data (database queries, API calls)
- Access backend resources directly
- Keep sensitive logic (tokens, API keys) off the client
- Render large dependencies that don't need interactivity

### Use Client Components when you need

- Event handlers (onClick, onChange, onSubmit)
- State (useState, useReducer)
- Effects (useEffect, useLayoutEffect)
- Browser APIs (window, document, localStorage)
- Custom hooks that depend on state or effects

### The boundary pattern

Push `"use client"` as far down the tree as possible. Instead of making an entire page a client component because one button needs state, extract just the interactive part:

```
app/
  dashboard/
    page.tsx          ← Server Component (fetches data)
    components/
      stats-grid.tsx  ← Server Component (renders data)
      filter-bar.tsx  ← Client Component (has state)
```

**Example — composing server and client:**
```tsx
// app/dashboard/page.tsx (Server Component)
import { FilterBar } from './components/filter-bar'
import { StatsGrid } from './components/stats-grid'

export default async function DashboardPage() {
  const stats = await getStats()
  return (
    <div className="p-6">
      <FilterBar />
      <StatsGrid data={stats} />
    </div>
  )
}
```

```tsx
// app/dashboard/components/filter-bar.tsx
"use client"
import { useState } from 'react'

export function FilterBar() {
  const [filter, setFilter] = useState('all')
  return (
    <div className="flex gap-2 mb-4">
      {['all', 'active', 'archived'].map(f => (
        <button
          key={f}
          onClick={() => setFilter(f)}
          className={`px-3 py-1 rounded ${filter === f ? 'bg-blue-600 text-white' : 'bg-gray-100'}`}
        >
          {f}
        </button>
      ))}
    </div>
  )
}
```

## Project Structure

Organize by feature/route rather than by type. This scales better as projects grow because related files stay together.

```
app/
  (marketing)/          ← Route group (no URL segment)
    page.tsx            ← Landing page
    pricing/page.tsx
    about/page.tsx
    layout.tsx          ← Marketing-specific layout
  (app)/                ← Route group for authenticated app
    dashboard/
      page.tsx
      loading.tsx
      components/       ← Route-specific components
    settings/
      page.tsx
    layout.tsx          ← App layout with sidebar/nav
  api/
    [...route]/route.ts ← API routes
  layout.tsx            ← Root layout
  globals.css

components/             ← Shared components used across routes
  ui/                   ← Generic UI primitives (button, card, input)
  forms/                ← Reusable form components
lib/
  db.ts                 ← Database client
  auth.ts               ← Auth utilities
  utils.ts              ← General helpers
types/
  index.ts              ← Shared TypeScript types
```

### Route groups

Use `(groupName)` to organize routes without affecting the URL. Common patterns:
- `(marketing)` vs `(app)` — different layouts for public/authenticated sections
- `(auth)` — group login/register/forgot-password under one layout

### Colocation

Keep route-specific components, utils, and types alongside the route. Only promote to top-level `components/` or `lib/` when something is genuinely shared across multiple routes.

## Layouts and Templates

### Layouts persist across navigations

```tsx
// app/(app)/layout.tsx
export default function AppLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen">
      <Sidebar />
      <main className="flex-1 overflow-auto">{children}</main>
    </div>
  )
}
```

Layouts don't remount on navigation — their state is preserved. Use this for persistent UI like sidebars, navbars, and tab bars.

### Templates remount on every navigation

Use `template.tsx` instead of `layout.tsx` when you need fresh state on each navigation (e.g., enter/exit animations, per-page analytics logging).

## Data Fetching

### Server Components — fetch directly

```tsx
// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 } // cache for 1 hour
  })
  return res.json()
}

export default async function ProductsPage() {
  const products = await getProducts()
  return <ProductList products={products} />
}
```

### Caching strategies

- `{ cache: 'force-cache' }` — cache indefinitely (default for GET in production)
- `{ cache: 'no-store' }` — always fresh, never cached
- `{ next: { revalidate: N } }` — cache for N seconds, then revalidate in the background
- `{ next: { tags: ['products'] } }` — tag-based on-demand revalidation with `revalidateTag('products')`

### Server Actions for mutations

```tsx
// app/products/actions.ts
"use server"
import { revalidatePath } from 'next/cache'

export async function createProduct(formData: FormData) {
  const name = formData.get('name') as string
  await db.product.create({ data: { name } })
  revalidatePath('/products')
}
```

Use Server Actions for form submissions and mutations. They run on the server, can access your database directly, and handle revalidation.

### Parallel data fetching

When a page needs multiple independent data sources, fetch them in parallel rather than sequentially:

```tsx
export default async function DashboardPage() {
  const [stats, activity, notifications] = await Promise.all([
    getStats(),
    getRecentActivity(),
    getNotifications(),
  ])

  return (
    <>
      <StatsGrid data={stats} />
      <ActivityFeed items={activity} />
      <NotificationList notifications={notifications} />
    </>
  )
}
```

## Loading and Error States

### loading.tsx — instant loading UI

```tsx
// app/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="animate-pulse space-y-4 p-6">
      <div className="h-8 w-48 bg-gray-200 rounded" />
      <div className="grid grid-cols-3 gap-4">
        {[1, 2, 3].map(i => (
          <div key={i} className="h-32 bg-gray-200 rounded-lg" />
        ))}
      </div>
    </div>
  )
}
```

Next.js automatically wraps your page in a `<Suspense>` boundary using `loading.tsx`, so users see the skeleton immediately while data loads.

### error.tsx — graceful error handling

```tsx
// app/dashboard/error.tsx
"use client"

export default function DashboardError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div className="flex flex-col items-center justify-center p-6">
      <h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
      <p className="text-gray-500 mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        Try again
      </button>
    </div>
  )
}
```

### not-found.tsx — custom 404 per route

Place `not-found.tsx` in any route segment to customize the 404 page for that section.

## Middleware

Use middleware for request-level logic that runs before any route is matched:

```tsx
// middleware.ts (root of project)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')

  if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
}
```

Common middleware use cases: authentication checks, redirects, A/B testing, geolocation-based routing, request logging.

## Dynamic Routes

```
app/
  products/
    [id]/
      page.tsx          ← /products/123
    [...slug]/
      page.tsx          ← /products/a/b/c (catch-all)
    [[...slug]]/
      page.tsx          ← /products or /products/a/b (optional catch-all)
```

### generateStaticParams for static generation

```tsx
export async function generateStaticParams() {
  const products = await getProducts()
  return products.map(p => ({ id: p.id }))
}
```

This pre-renders pages at build time for known params while falling back to dynamic rendering for unknown ones.

## Metadata

```tsx
// Static metadata
export const metadata = {
  title: 'Dashboard',
  description: 'View your analytics dashboard',
}

// Dynamic metadata
export async function generateMetadata({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)
  return {
    title: product.name,
    openGraph: { images: [product.image] },
  }
}
```

Use the root layout for global metadata (site name, default OG image) and individual pages for route-specific metadata.

