Next.js 15 App Router patterns for MERIDIAN. Use this skill whenever creating pages, layouts, route handlers, server/client components, loading states, error boundaries, or working with the App Router. Also use when implementing TanStack Query providers, Suspense boundaries, or API routes.
Default to Server Components. Add "use client" only when needed:
API routes in src/app/api/[feature]/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const schema = z.object({ symbol: z.string(), quantity: z.number().positive() });
export async function POST(request: NextRequest) {
try {
const body = schema.parse(await request.json());
// ... handle
return NextResponse.json({ success: true, data: result });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors }, { status: 400 });
}
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
Root layout provides theme, fonts, and providers. Feature layouts are optional:
// src/app/layout.tsx — root
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<body className="bg-[#0a0e1a] text-[#f9fafb] font-sans antialiased">
<QueryProvider>
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-auto">{children}</main>
</div>
</QueryProvider>
</body>
</html>
);
}
Provider wraps the app. Queries use feature-specific hooks:
// src/hooks/use-portfolio.ts
"use client";
export function usePortfolio() {
return useQuery({
queryKey: ["portfolio"],
queryFn: () => fetch("/api/portfolio").then(r => r.json()),
refetchInterval: 60_000, // 60s
});
}
Each route segment gets error.tsx and loading.tsx:
// src/app/wire/error.tsx
"use client";
export default function WireError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div className="p-8 text-center">
<p className="text-red-400">Failed to load The Wire: {error.message}</p>
<button onClick={reset} className="mt-4 px-4 py-2 bg-blue-600 rounded">Retry</button>
</div>
);
}