๐Ÿ”ง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 --items nextjs-app-router

Or scan + install everything matching your stack with npx 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.