AutoSkills
All items
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.

Next.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-all
  • page.tsx is required to make a route. A folder without page.tsx is just structural.
  • Layouts don't re-render on navigation. Use template.tsx when 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 as children.
  • 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 cached
    • fetch(url, { next: { revalidate: 60 } }) → ISR-style, 60s
    • fetch(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 formData server-side. Don't trust the client.
  • revalidatePath/revalidateTag invalidate the cache after mutations. Without these the page shows stale data.
  • useActionState (was useFormState) for progressive enhancement + status UI.

Cache layers (v14/v15)

  1. Request Memoization — same fetch() in one render = one network call. Automatic.
  2. Data Cache — persistent across requests. Controlled by fetch options or unstable_cache().
  3. Full Route Cache — static HTML/RSC payload, built at build time or first request.
  4. 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 of layout.tsx — kills server rendering for the whole tree.
  • Reading cookies/headers in components without await cookies()/await headers() (v15: now async).
  • Forgetting revalidatePath after a mutation — page stays stale.
  • Calling Server Actions from useEffect — use <form action> or useActionState.
  • Treating params as sync in v15 — params and searchParams are now Promises; await them.