Skip to content

Next.js

The Next.js skill provides expertise in Next.js with App Router, Server Components, Server Actions, and full-stack development patterns.

  • React applications with SSR/SSG
  • Full-stack applications
  • App Router patterns
  • Working in app/ directory
app/
├── layout.tsx # Root layout
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── api/
│ └── users/
│ └── route.ts # API route
└── users/
├── page.tsx # Users page
└── [id]/
└── page.tsx # User detail
// app/users/page.tsx - Server Component (default)
async function UsersPage() {
const users = await db.users.findMany();
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET() {
const users = await db.users.findMany();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const data = await request.json();
const user = await db.users.create({ data });
return NextResponse.json(user, { status: 201 });
}
app/actions.ts
'use server';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
await db.users.create({ data: { name } });
revalidatePath('/users');
}
// app/users/new/page.tsx
import { createUser } from '@/app/actions';
export default function NewUserPage() {
return (
<form action={createUser}>
<input name="name" required />
<button type="submit">Create</button>
</form>
);
}
app/users/[id]/page.tsx
interface PageProps {
params: { id: string };
}
export default async function UserPage({ params }: PageProps) {
const user = await db.users.findUnique({
where: { id: params.id }
});
if (!user) {
notFound();
}
return <div>{user.name}</div>;
}
app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<nav>{/* Navigation */}</nav>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}
  1. Use Server Components by default
  2. Add ‘use client’ only when needed
  3. Colocate data fetching with components
  4. Use loading.tsx for suspense boundaries
  5. Implement proper error boundaries
// ❌ BAD: Hooks in Server Component
export default async function Page() {
const [state, setState] = useState(0); // Error!
return <div>{state}</div>;
}
// ✅ GOOD: Mark as Client Component
'use client';
export default function Page() {
const [state, setState] = useState(0);
return <div>{state}</div>;
}
// ❌ BAD: Entire page as Client Component
'use client';
export default function Page() {
const [count, setCount] = useState(0);
const data = await fetchData(); // Can't use await in Client Component
return (
<div>
<HeavyComponent />
<button onClick={() => setCount(c => c + 1)}>{count}</button>
</div>
);
}
// ✅ GOOD: Extract interactive parts only
export default async function Page() {
const data = await fetchData(); // Server Component can await
return (
<div>
<HeavyComponent data={data} /> {/* Server Component */}
<Counter /> {/* Small Client Component */}
</div>
);
}
// Counter.tsx
'use client';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ❌ BAD: No loading state
export default async function Page() {
const data = await slowFetch();
return <div>{data}</div>;
}
// ✅ GOOD: Add loading.tsx
// app/page.tsx
export default async function Page() {
const data = await slowFetch();
return <div>{data}</div>;
}
// app/loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}
// ❌ BAD: No revalidation
'use server';
export async function createUser(data: FormData) {
await db.users.create({ data: getData(data) });
// Page still shows old data
}
// ✅ GOOD: Revalidate path
'use server';
export async function createUser(data: FormData) {
await db.users.create({ data: getData(data) });
revalidatePath('/users');
redirect('/users');
}
export default async function Page() {
// Fetch in parallel
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
]);
return (
<div>
<UserList users={users} />
<PostList posts={posts} />
</div>
);
}
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await fetchUser(params.id);
const posts = await fetchUserPosts(user.id); // Depends on user
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
);
}
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<FastComponent />
</div>
);
}
async function SlowComponent() {
const data = await slowFetch();
return <div>{data}</div>;
}
// Revalidate every hour
export const revalidate = 3600;
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});
return <div>{JSON.stringify(data)}</div>;
}
app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function createPost(data: FormData) {
await db.posts.create({ data: getData(data) });
revalidatePath('/posts');
// or
revalidateTag('posts');
}
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'User Profile',
description: 'View user profile information',
};
export default function Page() {
return <div>Profile</div>;
}
// Dynamic metadata
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const user = await fetchUser(params.id);
return {
title: `${user.name}'s Profile`,
description: `Profile page for ${user.name}`,
};
}

See React skill for component patterns and hooks.

Full TypeScript integration:

import { NextRequest, NextResponse } from 'next/server';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
): Promise<NextResponse> {
const user = await fetchUser(params.id);
return NextResponse.json(user);
}