Next.js App Router skill for Codex

Folders, boundaries and handlers that behave as expected. Codex lays out app/ folders with the right file conventions and places loading.tsx and error.tsx where they actually catch the layout or page in question. It uses async params with generated PageProps types, builds intercepting-route modals that survive refresh, writes route handlers with auth inside, and checks the build's route table to confirm static or dynamic rendering per route.

  • Frontend
  • $nextjs-app-router
  • 187 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- nextjs-app-router

Installs to ~/.agents/skills/nextjs-app-router/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $nextjs-app-router, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds and restructures Next.js App Router routes — layouts, pages, route groups, dynamic and catch-all segments, parallel and intercepting routes, loading.tsx, error.tsx, not-found.tsx, metadata, and route.ts handlers — with async params and typed PageProps. Use when adding a route or layout in app/, when a loading or error file does not catch what you expect, when building a modal route, when writing an API endpoint as a route handler, or when upgrading routing code to Next.js 15 or 16.

Use it when

  • Adding pages, layouts or route groups under app/
  • Loading or error UI shows at the wrong level or not at all
  • Building parallel or intercepting routes
  • Writing HTTP endpoints as route handlers

Use something else when

How to check the result

next build passes and its route table shows each touched route with the intended rendering mode; direct loads, client navigations, forced errors and missing records render the expected UI.

The mistake it prevents

Expecting loading.tsx or error.tsx to cover the layout in the same folder; both wrap only the page and children below it.

~/.agents/skills/nextjs-app-router/SKILL.md

--- name: nextjs-app-router description: Builds and restructures Next.js App Router routes — layouts, pages, route groups, dynamic and catch-all segments, parallel and intercepting routes,… license: MIT ---

Done well means: each URL maps to a predictable folder, shared UI lives in the right layout, every segment that can be slow or fail has a loading or error boundary at the level users expect, params are awaited, and the build output shows the routes rendering the way you intended (static, dynamic, or partially prerendered).

Before changing routes

  1. Read AGENTS.md, package.json (Next.js version) and next.config.*.
  2. Note version-specific rules:
    • Next 15+: params, searchParams, cookies(), headers() and draftMode() are async. Next 16 removed the synchronous fallback.
    • Next 16: middleware.ts is renamed proxy.ts (exported function proxy); every parallel route slot needs an explicit default.tsx; next lint is gone (call ESLint directly).
    • Next 15.5+: next typegen and the build generate global PageProps<'/route'>, LayoutProps<'/route'> and RouteContext<'/route'> helpers.
  3. List the existing tree: find app -name "*.tsx" -o -name "*.ts" | sort (or src/app).
  4. Check whether cacheComponents is enabled; it changes how data access must be wrapped. For caching decisions use the nextjs-caching skill.

File conventions

FileRoleNotes
layout.tsxUI shared by a segment and childrenPersists across navigations; does not re-render or receive searchParams
page.tsxMakes the segment routableReceives params and searchParams promises
loading.tsxSuspense fallback for the segment’s page and childrenDoes not wrap the layout of the same segment
error.tsxError boundary for the segmentMust be a client component; gets error and reset
global-error.tsxCatches errors in the root layoutMust render its own <html> and <body>
not-found.tsxUI for notFound() and unmatched URLsRoot app/not-found.tsx handles global 404s
template.tsxLike layout but remounts on navigationUse for per-page enter animations or resetting state
default.tsxFallback for a parallel slot on hard navigationRequired per slot in Next 16
route.tsHTTP handlerCannot coexist with page.tsx in the same segment

Folder conventions: (group) organizes without a URL segment and can give sections different root layouts; _private opts a folder out of routing; [id] dynamic, [...slug] catch-all, [[...slug]] optional catch-all; @slot parallel route; (.), (..), (...) intercepting routes.

Pages and layouts

// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { getPost } from "@/lib/posts";

export async function generateStaticParams() {
  const posts = await listPublishedSlugs();
  return posts.map((slug) => ({ slug }));
}

export async function generateMetadata({ params }: PageProps<"/blog/[slug]">): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return post ? { title: post.title, description: post.excerpt } : {};
}

export default async function Page({ params }: PageProps<"/blog/[slug]">) {
  const { slug } = await params;
  const post = await getPost(slug); // wrap getPost in React.cache to share with metadata
  if (!post) notFound();
  return <Article post={post} />;
}

If the project is on a version without the generated helpers, type props inline: { params: Promise<{ slug: string }> }.

Layout rules:

  • The root layout must render <html> and <body>. Put providers in a separate client component, not in the layout file itself.
  • Layouts cannot read the current pathname on the server. Use usePathname() in a client child for active-link styling.
  • Data a layout and its page both need: fetch in both through a React.cache-wrapped function; do not try to pass props from layout to page.

Loading and error boundaries

  • loading.tsx in app/dashboard/ shows while dashboard/page.tsx and nested segments load, but the dashboard/layout.tsx itself renders outside that boundary. If the layout awaits slow data, the fallback never appears: move that fetch into a Suspense- wrapped child.
  • For finer streaming, add <Suspense> around individual slow components instead of relying on a single route-level loading.tsx.
  • error.tsx catches errors from its segment’s page and children, not from the layout in the same folder. To catch layout errors, place error.tsx in the parent segment.
  • notFound(), redirect() and permanentRedirect() throw special errors. Never call them inside a try block whose catch swallows everything; re-throw or call them after the try.
// app/dashboard/error.tsx
"use client";
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
  return (
    <div role="alert">
      <p>Something went wrong loading the dashboard.</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

In production, error.message from server components is replaced by a generic message; use error.digest to correlate with server logs.

Parallel and intercepting routes (modal pattern)

app/
  layout.tsx            // renders {children} and {modal}
  @modal/
    default.tsx         // export default () => null
    (.)photos/[id]/page.tsx   // modal when navigating from within the app
  photos/[id]/page.tsx  // full page on direct load or refresh
  • The layout receives each slot as a prop: { children, modal }.
  • Close the modal with router.back(). Add a catch-all @modal/[...catchAll]/page.tsx returning null if other navigations should dismiss it.
  • Without default.tsx, a hard refresh on an unrelated URL 404s (and Next 16 fails the build).

Route handlers

// app/api/projects/[id]/route.ts
import { NextResponse, type NextRequest } from "next/server";

export async function GET(_req: NextRequest, ctx: RouteContext<"/api/projects/[id]">) {
  const { id } = await ctx.params;
  const project = await findProject(id);
  if (!project) return NextResponse.json({ error: "Not found" }, { status: 404 });
  return NextResponse.json(project);
}
  • GET handlers are not cached by default since Next 15.
  • Authenticate inside the handler. Do not rely on proxy.ts/middleware.ts alone for authorization: matchers drift and it is a single point of bypass.
  • Prefer server actions for mutations triggered by your own UI; use route handlers for webhooks, third-party callers, file downloads and non-React clients.
  • <Link> prefetches in production when visible. For a big list of links, set prefetch={false} on low-value ones to limit requests.
  • useRouter, usePathname, useSearchParams, useParams come from next/navigation. Wrap components that call useSearchParams in <Suspense>; on a statically rendered page the build otherwise fails with a missing-Suspense client-rendering bailout error.
  • Use redirect() in server code; router.push() only in client event handlers.

Gotchas

  • searchParams makes a page dynamic. Read it only in the components that need it.
  • A route.ts and page.tsx in the same folder is a build error.
  • Route groups with different root layouts cause a full page load when navigating between them.
  • generateStaticParams in a layout applies to all child pages; a forgotten one causes unexpected build-time fetching.
  • Mixing src/app and app at the root: Next uses app and ignores src/app.

Verification

  • next build passes. Read the route table it prints and confirm each route’s rendering mode (static, dynamic, partial) matches intent.
  • Visit each new URL directly (hard load) and via client navigation.
  • Force an error (throw in the page) and a missing record to see error.tsx and not-found.tsx render at the right level, then remove the test throw.
  • Throttle network and confirm loading UI appears where expected.
  • For modals: open from the list, refresh on the modal URL, use the back button.

Output

Report the folder tree you added or changed, which segments got loading/error boundaries and why, and the rendering mode of each touched route from the build output.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Next.js App Router"
  short_description: "Folders, boundaries and handlers that behave as expected"
  default_prompt: "$nextjs-app-router Add a photo modal that opens over the gallery but is a full page on refresh"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem