Use when building Next.js pages, layouts, Server Components, Server Actions, configuring fonts, metadata, or environment variables.
App Router patterns — Server Components by default, minimal client footprint.
// src/app/layout.tsx
import { sans, mono } from "@/assets/fonts";
import { cn } from "@/lib/utils";
import type { PropsWithChildren } from "react";
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en" suppressHydrationWarning>
<body className={cn(sans.variable, mono.variable)}>{children}</body>
</html>
);
}
suppressHydrationWarning on <html> — prevents theme provider mismatches<body> only — never add classes to <html>body {
@apply min-h-screen bg-background text-foreground font-sans antialiased;
}
Define all fonts in src/assets/fonts/index.ts, export as named constants:
// src/assets/fonts/index.ts
import { Geist, Geist_Mono } from "next/font/google";
export const sans = Geist({ subsets: ["latin"], variable: "--font-sans" });
export const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono" });
"use client" BoundaryEvery component is a Server Component by default. Add "use client" only when needed: useState, useEffect, event handlers, browser APIs. Push the boundary as deep as possible.
// ❌ forces entire page tree to client
"use client"
export default function Page() {
return <div><StaticContent /><InteractiveButton /></div>
}
// ✅ isolate only the interactive part
export default function Page() {
return <div><StaticContent /><InteractiveButton /></div>
}
// interactive-button.tsx
"use client"
export function InteractiveButton() { ... }
Fetch directly in Server Components — no API routes needed for internal data:
export default async function UserPage({ params }: { params: { id: string } }) {
const [err, user] = await safe(fetchUser(params.id));
if (err) return <Error />;
return <UserCard user={user} />;
}
Use React Query or SWR only in Client Components that need real-time updates or optimistic mutations.
Use Server Actions for mutations — not API routes:
async function createPost(formData: FormData) {
"use server";
const result = createPostSchema.safeParse(Object.fromEntries(formData));
if (!result.success) return { error: z.prettifyError(result.error) };
await db.insert(posts).values(result.data);
}
safeParse on user input — never parse{ error } on failure — never throw inside Server Actionstry/catch — use the safe() tuple pattern for async callsAlways define metadata or generateMetadata — never use <head> tags directly:
// static
export const metadata: Metadata = {
title: "Page Title",
description: "Page description",
};
// dynamic
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await getPost(params.slug);
return { title: post.title };
}
Use @t3-oss/env-nextjs (createEnv) for build-time Zod validation — never access process.env directly:
// src/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
RESEND_API_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
RESEND_API_KEY: process.env.RESEND_API_KEY,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
},
});
"use client" at page level — isolate only the interactive leaf component<head> tags — always use export const metadata or generateMetadataprocess.env directly — always import from src/env.tsparse on user input — always safeParse in Server ActionsclassName — put @apply rules in CSS, not the layout component