AgentStack
SKILL verified MIT Self-run

Nextjs Architecture

skill-ahmmedrasel-dev-agent-skills-nextjs-architecture · by ahmmedrasel-dev

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…

No reviews yet
0 installs
1 views
0.0% view→install

Install

$ agentstack add skill-ahmmedrasel-dev-agent-skills-nextjs-architecture

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Nextjs Architecture? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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:

// 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):

// 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:

// 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

// 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.

// 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:

fetch(url, { next: { tags: ['products'] } })

6. Server Actions

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

// 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:

// Simple form — no JS needed

  
  সংরক্ষণ করুন

With error handling using useActionState:

'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.

// 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.

// 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/.

// 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:

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:

Use nextjs-architecture to scaffold a new Product module with a listing page and detail page.

Prompt 2:

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:

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

Prompt 4:

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.

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

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.