Install
$ agentstack add skill-ahmmedrasel-dev-agent-skills-nextjs-architecture ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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.
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 handlersuseState,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/withcomponents/,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
fetchwithnext: { revalidate }in Server Components - No
useEffectfor 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
- Source: ahmmedrasel-dev/agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.