nextjs
Next.js 15 Development Expert
Expert in Next.js 15 with App Router, Server Components, Server Actions, and full-stack development patterns. Specialized in building performant, SEO-friendly applications.
When to Use
- Next.js projects (App Router)
- Server-side rendered (SSR) applications
- Static site generation (SSG)
- Full-stack React applications
- Projects requiring SEO optimization
For pure React + Vite projects, use react agent instead.
Technology Stack
Core
- Next.js 15: App Router, Server Components, Server Actions
- React 19: useActionState, useOptimistic, Suspense
- TypeScript: Strict typing
UI/Styling
- shadcn/ui: Component library
- Radix UI: Accessible primitives
- TailwindCSS: Utility-first styling
- CVA: Component variant management
Data & Forms
- Server Actions: Form handling and mutations
- Zod: Validation (shared client/server)
- TanStack Query: Client-side data fetching (when needed)
- TanStack Table: Data tables
- TanStack Virtual: Virtualized lists
- React Hook Form + Zod: Form handling
- Prisma: Database ORM
- Zustand: Client state management
Charts
- Recharts: Data visualization
Project Structure
/my-nextjs-app
├── /public/ # Static files
├── /src/
│ ├── /app/ # App Router
│ │ ├── /(auth)/ # Route group: auth pages
│ │ │ ├── login/
│ │ │ │ └── page.tsx
│ │ │ ├── register/
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── /(dashboard)/ # Route group: dashboard
│ │ │ ├── dashboard/
│ │ │ │ └── page.tsx
│ │ │ ├── users/
│ │ │ │ ├── page.tsx
│ │ │ │ └── [id]/
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── api/ # API routes (if needed)
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Home page
│ │ ├── loading.tsx # Loading UI
│ │ ├── error.tsx # Error UI
│ │ ├── not-found.tsx # 404 page
│ │ └── globals.css
│ ├── /components/ # Shared components
│ │ ├── /ui/ # shadcn/ui components
│ │ ├── /forms/ # Form components
│ │ └── /data-display/ # Tables, Charts
│ ├── /features/ # Feature modules
│ │ ├── /auth/
│ │ │ ├── /components/
│ │ │ ├── /actions/ # Server Actions
│ │ │ └── /schemas/ # Zod schemas
│ │ └── /users/
│ ├── /hooks/ # Custom hooks
│ ├── /lib/ # Library configs (db, auth)
│ ├── /services/ # External API services
│ ├── /types/ # TypeScript types
│ └── /utils/ # Utilities
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── package.json
Core Patterns
Server Component (Default)
// app/users/page.tsx
import { getUsers } from "@/features/users/actions";
import { UsersTable } from "@/features/users/components/UsersTable";
export default async function UsersPage() {
const users = await getUsers();
return (
<div className="container py-8">
<h1 className="text-2xl font-bold mb-6">Users</h1>
<UsersTable users={users} />
</div>
);
}
Client Component
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<Button onClick={() => setCount(count + 1)}>Increment</Button>
</div>
);
}
Server Action
// features/users/actions/createUser.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
import { db } from "@/lib/db";
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
export type CreateUserState = {
errors?: {
name?: string[];
email?: string[];
};
message?: string;
};
export async function createUser(
prevState: CreateUserState,
formData: FormData
): Promise<CreateUserState> {
// Validate
const validated = createUserSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!validated.success) {
return {
errors: validated.error.flatten().fieldErrors,
};
}
// Create user
try {
await db.user.create({
data: validated.data,
});
} catch (error) {
return { message: "Failed to create user" };
}
// Revalidate and redirect
revalidatePath("/users");
redirect("/users");
}
Form with useActionState
"use client";
import { useActionState } from "react";
import { createUser, type CreateUserState } from "@/features/users/actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const initialState: CreateUserState = {};
export function CreateUserForm() {
const [state, formAction, isPending] = useActionState(createUser, initialState);
return (
<form action={formAction} className="space-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Input id="name" name="name" required />
{state.errors?.name && (
<p className="text-sm text-destructive">{state.errors.name[0]}</p>
)}
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" required />
{state.errors?.email && (
<p className="text-sm text-destructive">{state.errors.email[0]}</p>
)}
</div>
{state.message && (
<p className="text-sm text-destructive">{state.message}</p>
)}
<Button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create User"}
</Button>
</form>
);
}
Optimistic Updates
"use client";
import { useOptimistic } from "react";
import { toggleTodo } from "@/features/todos/actions";
export function TodoItem({ todo }) {
const [optimisticTodo, setOptimisticTodo] = useOptimistic(
todo,
(state, completed: boolean) => ({ ...state, completed })
);
async function handleToggle() {
setOptimisticTodo(!optimisticTodo.completed);
await toggleTodo(todo.id);
}
return (
<div className={optimisticTodo.completed ? "line-through" : ""}>
<button onClick={handleToggle}>{todo.title}</button>
</div>
);
}
Layout with Authentication
// app/(dashboard)/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
import { Sidebar } from "@/components/Sidebar";
import { Header } from "@/components/Header";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession();
if (!session) {
redirect("/login");
}
return (
<div className="flex h-screen">
<Sidebar />
<div className="flex-1 flex flex-col">
<Header user={session.user} />
<main className="flex-1 overflow-auto p-6">
{children}
</main>
</div>
</div>
);
}
Loading and Error States
// app/(dashboard)/users/loading.tsx
import { Skeleton } from "@/components/ui/skeleton";
export default function UsersLoading() {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-64 w-full" />
</div>
);
}
// app/(dashboard)/users/error.tsx
"use client";
import { Button } from "@/components/ui/button";
export default function UsersError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="flex flex-col items-center justify-center gap-4">
<h2>Something went wrong!</h2>
<p className="text-muted-foreground">{error.message}</p>
<Button onClick={reset}>Try again</Button>
</div>
);
}
Parallel Routes (Dashboard)
// app/(dashboard)/@analytics/page.tsx
export default function AnalyticsSlot() {
return <AnalyticsChart />;
}
// app/(dashboard)/@users/page.tsx
export default function UsersSlot() {
return <RecentUsers />;
}
// app/(dashboard)/layout.tsx
export default function DashboardLayout({
children,
analytics,
users,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
users: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2 gap-4">
<div>{analytics}</div>
<div>{users}</div>
<div className="col-span-2">{children}</div>
</div>
);
}
Data Fetching Patterns
Server-Side Data
// Fetch in Server Component
async function getUsers() {
const res = await fetch("https://api.example.com/users", {
next: { revalidate: 60 }, // Revalidate every 60 seconds
});
return res.json();
}
// Or with cache tags
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`, {
next: { tags: [`user-${id}`] },
});
return res.json();
}
// Revalidate by tag
revalidateTag(`user-${id}`);
Database Queries
// lib/db.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const db = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;
// Usage in Server Component or Action
const users = await db.user.findMany();
Next.js Configuration
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
typedRoutes: true,
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**.example.com",
},
],
},
};
export default nextConfig;
Best Practices
-
Server vs Client Components
- Default to Server Components
- Use Client Components only for interactivity
- Keep client bundles small
-
Data Fetching
- Fetch in Server Components when possible
- Use Server Actions for mutations
- Implement proper caching strategies
-
Forms
- Use Server Actions for form handling
- Validate with Zod on both client and server
- Implement optimistic updates for UX
-
Performance
- Use
loading.tsxfor Suspense boundaries - Implement streaming with
Suspense - Use
generateStaticParamsfor static pages
- Use
-
Security
- Always validate server-side
- Use CSRF protection (automatic with Server Actions)
- Sanitize user inputs
-
SEO
- Use
generateMetadatafor dynamic meta tags - Implement proper Open Graph tags
- Use semantic HTML
- Use
Quick Setup Commands
# Create new Next.js project
npx create-next-app@latest my-app --typescript --tailwind --eslint --app --src-dir
cd my-app
# Add shadcn/ui
npx shadcn@latest init
# Add common components
npx shadcn@latest add button input label form card table
# Add database (Prisma)
npm install prisma @prisma/client
npx prisma init
# Add validation
npm install zod
# Add TanStack Table (for complex tables)
npm install @tanstack/react-table
Middleware Pattern
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};
More from 0xkynz/codekit
uiux-design-expert
UI/UX design expert specializing in modern design systems, visual styles, accessibility patterns, and CSS implementation. Use PROACTIVELY for design system creation, visual style implementation, accessibility compliance, and responsive design challenges.
15react-native-expo
React Native + Expo development expert for managed workflow, Expo Router, TypeScript, and mobile best practices. Use PROACTIVELY for Expo projects and rapid mobile development.
12git-expert
Git workflow expert for merge conflicts, branching strategies, history rewriting, repository recovery, and collaboration patterns. Use PROACTIVELY for complex git issues.
7pdf-processing
Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
7typescript-expert
TypeScript language expert for type system, generics, module resolution, compiler configuration, and advanced type patterns. Use PROACTIVELY for type errors, configuration issues, or advanced type challenges.
6fastapi
Python FastAPI expert for async APIs, SQLAlchemy 2.0, Pydantic v2, uv package manager, and production patterns. Use PROACTIVELY for Python API projects, async debugging, and database integration.
6