skillv1.0.0
nextjs-app-router
Next.js 14+ App Router conventions, data fetching, Server Actions, caching layers
nextjsreacttypescriptfrontendfullstack
Install
$npx @autoskills-dev/autoskills --items nextjs-app-router
Or scan + install everything matching your stack with npx @autoskills-dev/autoskills.
Source
View on GitHubNext.js App Router
Pages Router and App Router are two different frameworks. If you're mixing advice from both, your code will be subtly wrong.
File conventions
app/
layout.tsx # wraps every page below; persistent across navigations
page.tsx # the actual route component
loading.tsx # Suspense fallback shown while page renders
error.tsx # error boundary (must be 'use client')
not-found.tsx # 404 for this segment
route.ts # API route handler (GET/POST/...)
template.tsx # like layout but re-mounts on navigation
(group)/ # parens = route group, doesn't affect URL
[param]/ # dynamic segment
[...slug]/ # catch-all
[[...slug]]/ # optional catch-allpage.tsxis required to make a route. A folder withoutpage.tsxis just structural.- Layouts don't re-render on navigation. Use
template.tsxwhen you actually need re-mount.
Server vs Client Components
- Default: Server Component. No
'use client'needed. 'use client'is contagious downward, not upward. A client component can render server components passed aschildren.- The serialization boundary matters. Props from server → client must be serializable (no functions, no Dates with custom prototypes, no class instances).
Data fetching
// app/products/page.tsx — Server Component
export default async function Page() {
const products = await db.products.findMany();
return <ProductList products={products} />;
}- No
getServerSideProps/getStaticProps. Those are Pages Router. fetch()is patched — extends with caching options:fetch(url)→ cached indefinitely (default)fetch(url, { cache: 'no-store' })→ never cachedfetch(url, { next: { revalidate: 60 } })→ ISR-style, 60sfetch(url, { next: { tags: ['products'] } })→ tag-based invalidation
Server Actions
// app/products/actions.ts
'use server';
export async function createProduct(formData: FormData) {
const name = formData.get('name') as string;
await db.products.create({ name });
revalidatePath('/products');
}// app/products/page.tsx
<form action={createProduct}>
<input name="name" />
<button>Create</button>
</form>- Always validate
formDataserver-side. Don't trust the client. revalidatePath/revalidateTaginvalidate the cache after mutations. Without these the page shows stale data.useActionState(wasuseFormState) for progressive enhancement + status UI.
Cache layers (v14/v15)
- Request Memoization — same
fetch()in one render = one network call. Automatic. - Data Cache — persistent across requests. Controlled by
fetchoptions orunstable_cache(). - Full Route Cache — static HTML/RSC payload, built at build time or first request.
- Router Cache — client-side, per-segment, in browser memory.
When data goes stale: revalidatePath, revalidateTag, or router.refresh() from a client component.
Metadata
export const metadata: Metadata = { title: 'Products' };
// or dynamic:
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await getProduct(params.id);
return { title: product.name };
}Middleware
middleware.ts at the project root, runs on the Edge. Use for: auth redirects, A/B routing, rewrites. Don't use for: heavy logic, database calls (Edge runtime has restrictions).
Streaming UI
Wrap slow children in <Suspense> — the fast parts ship first.
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>loading.tsx is a route-level Suspense boundary for the whole page.tsx.
Common mistakes
- Putting
'use client'at the top oflayout.tsx— kills server rendering for the whole tree. - Reading cookies/headers in components without
await cookies()/await headers()(v15: now async). - Forgetting
revalidatePathafter a mutation — page stays stale. - Calling Server Actions from
useEffect— use<form action>oruseActionState. - Treating
paramsas sync in v15 —paramsandsearchParamsare now Promises;awaitthem.