# Nextjs Architecture

> Guide Next.js 14+ App Router projects using module-based architecture, server-first component strategy, native fetch with revalidation, server actions for mutations, TanStack Query for client-side data, and Zustand for UI state. Use when asked about Next.js folder structure, where to put components, how to fetch data from a Laravel API, when to use server vs client components, how to write server…

- **Type:** Skill
- **Install:** `agentstack add skill-ahmmedrasel-dev-agent-skills-nextjs-architecture`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ahmmedrasel-dev](https://agentstack.voostack.com/s/ahmmedrasel-dev)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ahmmedrasel-dev](https://github.com/ahmmedrasel-dev)
- **Source:** https://github.com/ahmmedrasel-dev/agent-skills/tree/main/skills/nextjs-architecture

## Install

```sh
agentstack add skill-ahmmedrasel-dev-agent-skills-nextjs-architecture
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Next.js Architecture

## When to use

Use this skill when working on a Next.js 14+ App Router project that consumes a Laravel REST API backend. Most useful when:

- Starting a new feature module (e.g. Product, Cart, Order)
- Deciding between Server Component and Client Component
- Writing data fetching functions with proper caching
- Writing Server Actions for form mutations
- Setting up TanStack Query for interactive client-side data
- Deciding what belongs in Zustand vs server state

## Input parameters

- Module name or feature being built (e.g. `product`, `cart`, `order`)
- Whether it is a new module or modifying an existing one
- The specific concern: component, data fetching, server action, caching, or state management
- Whether the data is public, authenticated, or admin-only

## Procedure

### 1. Project Structure

```
src/
├── app/                          ← App Router pages (thin shells only)
│   ├── (public)/                 ← No auth required
│   │   ├── products/
│   │   │   ├── page.tsx
│   │   │   └── [slug]/page.tsx
│   ├── (auth)/                   ← Login, register
│   ├── (dashboard)/              ← Protected — auth check in layout
│   │   └── layout.tsx
│   └── admin/                    ← Admin panel
│
├── modules/                      ← All feature logic lives here
│   ├── product/
│   │   ├── components/           ← UI components for this feature
│   │   ├── actions/              ← Server Actions (mutations)
│   │   ├── hooks/                ← Client hooks (TanStack Query etc.)
│   │   ├── types/                ← TypeScript interfaces
│   │   └── api.ts                ← Server-side fetch functions
│   ├── cart/
│   ├── order/
│   └── auth/
│
├── components/                   ← Shared UI
│   ├── ui/                       ← Shadcn/ui base components
│   └── layout/                   ← Header, Footer, Sidebar
│
├── lib/
│   ├── api.ts                    ← Base fetch wrapper
│   └── auth.ts                   ← Server-side token helper
│
├── stores/                       ← Zustand stores (UI state only)
│   ├── cart.store.ts
│   └── ui.store.ts
│
└── types/                        ← Global TypeScript types
    └── api.ts                    ← PaginatedResponse, etc.
```

App Router pages are **thin shells** — they only import from `modules/` and render layout. No logic lives in `app/`.

### 2. Component Strategy

Decision rule — add `'use client'` only if the component needs:
- `onClick`, `onChange`, form event handlers
- `useState`, `useEffect`, `useRef`
- Browser APIs (`window`, `localStorage`)
- TanStack Query hooks or Zustand store

Everything else is a **Server Component by default** (no directive needed).

**Server Component:**
```tsx
// modules/product/components/ProductCard.tsx
// No directive — Server Component by default

import Image from 'next/image';
import Link from 'next/link';
import { Product } from '../types/product.types';

export default function ProductCard({ product }: { product: Product }) {
  return (
    
      
      {product.name}
      ৳{product.price}
      বিস্তারিত দেখুন
    
  );
}
```

**Client Component (opt-in):**
```tsx
// modules/product/components/AddToCartButton.tsx
'use client';

import { useCartStore } from '@/stores/cart.store';

export default function AddToCartButton({ product }) {
  const addItem = useCartStore((s) => s.addItem);
  return  addItem(product)}>কার্টে যোগ করুন;
}
```

**Pattern — Server Component wraps Client Components:**
```tsx
// app/(public)/products/[slug]/page.tsx  ← Server Component
import { getProductBySlug } from '@/modules/product/api';
import ProductGallery from '@/modules/product/components/ProductGallery';   // 'use client'
import AddToCartButton from '@/modules/product/components/AddToCartButton'; // 'use client'

export default async function ProductPage({ params }) {
  const product = await getProductBySlug(params.slug); // fetch on server
  if (!product) notFound();

  return (
    
      
      {product.name}
      
    
  );
}
```

### 3. Base Fetch Wrapper

```ts
// lib/api.ts
const API_BASE = process.env.API_URL ?? 'http://localhost:8000/api/v1';

export async function apiFetch(
  endpoint: string,
  options: RequestInit & { token?: string } = {},
): Promise {
  const { token, ...rest } = options;

  const res = await fetch(`${API_BASE}${endpoint}`, {
    ...rest,
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    },
  });

  if (!res.ok) {
    const error = await res.json().catch(() => ({}));
    throw new Error(error.message ?? `API error: ${res.status}`);
  }

  return res.json() as Promise;
}
```

### 4. Data Fetching — Module API Functions

Lives in `modules//api.ts` — server-side only, never imported by client components.

```ts
// modules/product/api.ts
import { apiFetch } from '@/lib/api';
import { Product, PaginatedProducts } from './types/product.types';

export async function getProducts(params?: {
  category_id?: number;
  search?: string;
  page?: number;
}): Promise {
  const query = new URLSearchParams(
    Object.entries(params ?? {})
      .filter(([, v]) => v != null)
      .map(([k, v]) => [k, String(v)])
  ).toString();

  return apiFetch(`/products${query ? `?${query}` : ''}`, {
    next: { revalidate: 60 },           // cache 60s
  });
}

export async function getProductBySlug(slug: string): Promise {
  try {
    const res = await apiFetch(`/products/${slug}`, {
      next: { revalidate: 300 },        // cache 5 min
    });
    return res.data;
  } catch {
    return null;
  }
}

export async function getAdminProducts(): Promise {
  return apiFetch('/admin/products', {
    cache: 'no-store',                  // never cache admin data
  });
}
```

### 5. Caching Strategy

| Data type | Option | Example |
|-----------|--------|---------|
| Product listing | `revalidate: 60` | Refreshes every 60s |
| Product detail | `revalidate: 300` | 5 min cache |
| Categories | `revalidate: 3600` | Rarely changes |
| Admin data | `no-store` | Always fresh |
| User orders | `no-store` | Personal, never cache |
| Static pages | `force-cache` | Terms, about |

Tag-based revalidation — add tags to fetch, then invalidate by tag after mutation:
```ts
fetch(url, { next: { tags: ['products'] } })
```

### 6. Server Actions

Server Actions handle all form mutations. Lives in `modules//actions/`.

```ts
// modules/product/actions/product.actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { apiFetch } from '@/lib/api';
import { getServerToken } from '@/lib/auth';

export async function createProductAction(formData: FormData) {
  const token = await getServerToken();

  const payload = {
    name:        formData.get('name') as string,
    description: formData.get('description') as string,
    price:       Number(formData.get('price')),
    category_id: Number(formData.get('category_id')),
    stock:       Number(formData.get('stock')),
  };

  try {
    await apiFetch('/admin/products', {
      method: 'POST',
      body: JSON.stringify(payload),
      token,
    });
  } catch (error) {
    return { error: (error as Error).message };
  }

  revalidatePath('/products');
  revalidateTag('products');
  redirect('/admin/products');
}

export async function deleteProductAction(id: number) {
  const token = await getServerToken();
  await apiFetch(`/admin/products/${id}`, { method: 'DELETE', token });
  revalidatePath('/admin/products');
  revalidateTag('products');
}
```

Using in a form:
```tsx
// Simple form — no JS needed

  
  সংরক্ষণ করুন

```

With error handling using `useActionState`:
```tsx
'use client';
import { useActionState } from 'react';

export default function ProductForm() {
  const [state, action, isPending] = useActionState(createProductAction, null);
  return (
    
      {state?.error && {state.error}}
      
      
        {isPending ? 'সংরক্ষণ হচ্ছে...' : 'সংরক্ষণ করুন'}
      
    
  );
}
```

### 7. TanStack Query — Client-side Only

Use **only** when data needs live updates, optimistic UI, or polling. Never use for data that can be fetched on the server.

```ts
// modules/product/hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';

export function useProducts(filters: Record) {
  const query = new URLSearchParams(filters).toString();
  return useQuery({
    queryKey: ['products', filters],
    queryFn: async () => {
      const res = await fetch(
        `${process.env.NEXT_PUBLIC_API_URL}/products?${query}`,
        { headers: { Accept: 'application/json' } }
      );
      if (!res.ok) throw new Error('Failed');
      return res.json();
    },
    staleTime: 1000 * 60,
  });
}
```

### 8. Zustand — UI State Only

Never store server data in Zustand. Only UI state: cart items, sidebar open, modal state, toast queue.

```ts
// stores/cart.store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCartStore = create()(
  persist(
    (set, get) => ({
      items: [],
      addItem: (item) => set((state) => {
        const existing = state.items.find((i) => i.id === item.id);
        if (existing) {
          return { items: state.items.map((i) =>
            i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
          )};
        }
        return { items: [...state.items, { ...item, quantity: 1 }] };
      }),
      removeItem: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
      clearCart: () => set({ items: [] }),
      totalItems: () => get().items.reduce((sum, i) => sum + i.quantity, 0),
      totalPrice: () => get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
    }),
    { name: 'cart-storage' }
  )
);
```

### 9. TypeScript Types

Mirror Laravel API Resources exactly. Lives in `modules//types/`.

```ts
// modules/product/types/product.types.ts
export interface Product {
  id: number;
  name: string;
  slug: string;
  description: string | null;
  price: number;
  stock: number;
  status: 'active' | 'inactive' | 'draft';
  category: Category | null;
  variants: ProductVariant[];
  created_at: string;
}

// Matches Laravel ResourceCollection
export interface PaginatedProducts {
  data: Product[];
  links: { first: string; last: string; prev: string | null; next: string | null };
  meta: { current_page: number; last_page: number; per_page: number; total: number };
}
```

Global in `types/api.ts`:
```ts
export interface PaginatedResponse {
  data: T[];
  links: { first: string; last: string; prev: string | null; next: string | null };
  meta: { current_page: number; last_page: number; per_page: number; total: number };
}
```

## Examples

Prompt 1:
```text
Use nextjs-architecture to scaffold a new Product module with a listing page and detail page.
```

Prompt 2:
```text
I need to add a delete button for orders in the admin panel. Should it be a Server Action or an API route? Use nextjs-architecture conventions.
```

Prompt 3:
```text
Review this component — does it follow nextjs-architecture? It's fetching data inside a useEffect.
```

Prompt 4:
```text
Use nextjs-architecture to set up category filter on the shop page with TanStack Query.
```

## Smoke test

Ask the agent to scaffold a `Cart` module. Verify the response:
- Creates `modules/cart/` with `components/`, `actions/`, `hooks/`, `types/`, `api.ts`
- Checkout form uses a Server Action (not a fetch inside `onClick`)
- Cart item count uses Zustand (not server state)
- Data fetching uses native `fetch` with `next: { revalidate }` in Server Components
- No `useEffect` for data fetching — only for side effects
- TypeScript types mirror the Laravel API Resource shape

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [ahmmedrasel-dev](https://github.com/ahmmedrasel-dev)
- **Source:** [ahmmedrasel-dev/agent-skills](https://github.com/ahmmedrasel-dev/agent-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-ahmmedrasel-dev-agent-skills-nextjs-architecture
- Seller: https://agentstack.voostack.com/s/ahmmedrasel-dev
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
