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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- nextjs-app-router Installs to ~/.agents/skills/nextjs-app-router/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install nextjs-app-router The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project nextjs-app-router Installs to .agents/skills/nextjs-app-router/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/nextjs-app-router.zipunzip -o nextjs-app-router.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) nextjs-app-router PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- Deciding what to cache and how to revalidate: use nextjs-caching
- Moving an existing pages/ app over: use nextjs-pages-to-app-migration
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.
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
- Read
AGENTS.md,package.json(Next.js version) andnext.config.*. - Note version-specific rules:
- Next 15+:
params,searchParams,cookies(),headers()anddraftMode()are async. Next 16 removed the synchronous fallback. - Next 16:
middleware.tsis renamedproxy.ts(exported functionproxy); every parallel route slot needs an explicitdefault.tsx;next lintis gone (call ESLint directly). - Next 15.5+:
next typegenand the build generate globalPageProps<'/route'>,LayoutProps<'/route'>andRouteContext<'/route'>helpers.
- Next 15+:
- List the existing tree:
find app -name "*.tsx" -o -name "*.ts" | sort(orsrc/app). - Check whether
cacheComponentsis enabled; it changes how data access must be wrapped. For caching decisions use thenextjs-cachingskill.
File conventions
| File | Role | Notes |
|---|---|---|
layout.tsx | UI shared by a segment and children | Persists across navigations; does not re-render or receive searchParams |
page.tsx | Makes the segment routable | Receives params and searchParams promises |
loading.tsx | Suspense fallback for the segment’s page and children | Does not wrap the layout of the same segment |
error.tsx | Error boundary for the segment | Must be a client component; gets error and reset |
global-error.tsx | Catches errors in the root layout | Must render its own <html> and <body> |
not-found.tsx | UI for notFound() and unmatched URLs | Root app/not-found.tsx handles global 404s |
template.tsx | Like layout but remounts on navigation | Use for per-page enter animations or resetting state |
default.tsx | Fallback for a parallel slot on hard navigation | Required per slot in Next 16 |
route.ts | HTTP handler | Cannot 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.tsxinapp/dashboard/shows whiledashboard/page.tsxand nested segments load, but thedashboard/layout.tsxitself 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-levelloading.tsx. error.tsxcatches errors from its segment’s page and children, not from the layout in the same folder. To catch layout errors, placeerror.tsxin the parent segment.notFound(),redirect()andpermanentRedirect()throw special errors. Never call them inside atryblock whosecatchswallows 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.tsxreturningnullif 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);
}
GEThandlers are not cached by default since Next 15.- Authenticate inside the handler. Do not rely on
proxy.ts/middleware.tsalone 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.
Navigation
<Link>prefetches in production when visible. For a big list of links, setprefetch={false}on low-value ones to limit requests.useRouter,usePathname,useSearchParams,useParamscome fromnext/navigation. Wrap components that calluseSearchParamsin<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
searchParamsmakes a page dynamic. Read it only in the components that need it.- A
route.tsandpage.tsxin the same folder is a build error. - Route groups with different root layouts cause a full page load when navigating between them.
generateStaticParamsin a layout applies to all child pages; a forgotten one causes unexpected build-time fetching.- Mixing
src/appandappat the root: Next usesappand ignoressrc/app.
Verification
-
next buildpasses. 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.tsxandnot-found.tsxrender 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.yaml
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.