skillv1.0.0
nextjs-app-router
Next.js 14+ App Router conventions, data fetching, Server Actions, caching layers
nextjsreacttypescriptfrontendfullstack
Install
$npx autoskills --items nextjs-app-router
Or scan + install everything matching your stack with npx 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.