2026-07-25 11:35:16 +07:00
---
name : nextjs
2026-07-26 13:27:48 +07:00
description : Use when building Next.js App Router applications — server components, client components, data fetching, routing, middleware, and deployment. Triggers from next.config, layout.tsx, page.tsx, and framework file patterns.
2026-07-25 11:35:16 +07:00
---
# Next.js Best Practices
## App Router Architecture
```
app/
├── (auth)/ # Route group — no URL segment
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
├── (dashboard)/
│ ├── layout.tsx # Shared layout for all dashboard pages
│ ├── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx
├── api/ # API route handlers
│ └── users/
│ └── route.ts
├── layout.tsx # Root layout
└── page.tsx # Home page (/)
```
## Server Components (Default)
**Every component in App Router is a Server Component by default.** Use Client Components only when needed.
Server components can:
- ✅ `async` component — `async function Page() { const data = await fetch(); ... }`
- ✅ Direct database access — `await db.select().from(users)`
- ✅ Import server-only modules (DB, filesystem, tokens)
- ✅ `{children}` render client components
```typescript
// ✅ Server Component — no "use client" directive
export default async function UserPage ({ params } : { params : Promise < { id : string } > }) {
const { id } = await params ;
const user = await db . select (). from ( users ). where ( eq ( users . id , Number ( id ))). limit ( 1 );
if ( ! user [ 0 ]) return notFound ();
return (
< div >
< h1 >{ user [ 0 ]. name }</ h1 >
< UserActions user = { user [ 0 ]} /> { /* Client component */ }
</ div >
);
}
```
## Client Components — When to Use
Add `'use client'` only when you need:
- 🔴 `useState` / `useReducer` — interactive UI state
- 🔴 `useEffect` — browser-side effects or synchronization
- 🔴 `useRouter` — programmatic navigation
- 🔴 Event handlers — `onClick` , `onSubmit` , `onChange`
- 🔴 Browser-only APIs — `localStorage` , `setInterval` , `IntersectionObserver`
- 🔴 Custom hooks that use any of the above
```typescript
'use client' ;
import { useState } from 'react' ;
export function UserActions ({ user } : { user : User }) {
const [ isEditing , setIsEditing ] = useState ( false );
return (
< button onClick = {() => setIsEditing ( true )}> Edit { user . name }</ button >
);
}
```
## Data Fetching Patterns
### Server-side (preferred)
```typescript
// Direct DB access in server component — no waterfall, no loading states
export default async function Dashboard() {
const [ stats , recentOrders , topUsers ] = await Promise . all ([
getStats (), getRecentOrders (), getTopUsers (),
]);
return < DashboardView stats = { stats } orders = { recentOrders } users = { topUsers } />;
}
```
### React Cache (deduplication)
```typescript
import { cache } from 'react' ;
export const getItem = cache ( async ( id : string ) => {
const item = await db . select (). from ( items ). where ( eq ( items . id , id )). limit ( 1 );
return item [ 0 ];
});
```
### Revalidation
```typescript
// Time-based (ISR)
export const revalidate = 3600 ; // seconds
// On-demand
import { revalidatePath , revalidateTag } from 'next/cache' ;
export async function updateUser ( formData : FormData ) {
'use server' ;
await db . update ( users ). set ({ name : formData.get ( 'name' ) }). where ( eq ( users . id , id ));
revalidatePath ( `/users/ ${ id } ` ); // Revalidate the page
revalidateTag ( 'users' ); // Revalidate all fetch calls with this tag
}
```
## Route Handlers (APIs)
```typescript
// app/api/users/route.ts
import { NextRequest , NextResponse } from 'next/server' ;
import { z } from 'zod' ;
const CreateUserSchema = z . object ({
email : z.string (). email (),
});
export async function POST ( request : NextRequest ) {
const body = await request . json ();
const parsed = CreateUserSchema . safeParse ( body );
if ( ! parsed . success ) {
return NextResponse . json ({ error : parsed.error.flatten () }, { status : 422 });
}
const user = await db . insert ( users ). values ( parsed . data ). returning ();
return NextResponse . json ( user [ 0 ], { status : 201 });
}
```
## Server Actions
```typescript
// app/users/actions.ts
'use server' ;
import { z } from 'zod' ;
import { db } from '@/db' ;
import { users } from '@/db/schema' ;
import { revalidatePath } from 'next/cache' ;
const CreateUserSchema = z . object ({
email : z.string (). email (),
name : z.string (). min ( 1 ),
});
export async function createUser ( formData : FormData ) {
const parsed = CreateUserSchema . safeParse ( Object . fromEntries ( formData ));
if ( ! parsed . success ) return { error : parsed.error.flatten () };
await db . insert ( users ). values ( parsed . data );
revalidatePath ( '/users' );
return { success : true };
}
```
## Middleware
```typescript
// middleware.ts
import { NextResponse } from 'next/server' ;
import type { NextRequest } from 'next/server' ;
export function middleware ( request : NextRequest ) {
const token = request . cookies . get ( 'session' ) ? . value ;
const { pathname } = request . nextUrl ;
// Protected routes
if ( pathname . startsWith ( '/dashboard' ) && ! token ) {
return NextResponse . redirect ( new URL ( '/login' , request . url ));
}
// Redirect logged-in users away from login
if ( pathname === '/login' && token ) {
return NextResponse . redirect ( new URL ( '/dashboard' , request . url ));
}
return NextResponse . next ();
}
export const config = {
matcher : [ '/dashboard/:path*' , '/login' ],
};
```
## Performance
- **Server Components** over Client Components whenever possible.
- **Streaming** — use `loading.tsx` and `Suspense` boundaries.
- **Image optimization** — `next/image` with `priority` for above-the-fold.
- **Font optimization** — `next/font` (self-hosted, no layout shift).
- **Bundle analysis** — `@next/bundle-analyzer` for tracking bloat.
- **Link prefetch** — `<Link>` prefetches by default. Disable with `prefetch={false}` for low-priority links.
## Testing
```typescript
// Vitest + Testing Library (not Next.js's built-in jest-config)
import { render , screen } from '@testing-library/react' ;
import Page from './page' ;
// Mock server component — render with test data
vi . mock ( '@/db' , () => ({ select : () => ({ from : () => ({ where : () => ({ limit : () => [ mockUser ] }) }) }) }));
describe ( 'UserPage' , () => {
it ( 'renders user name' , async () => {
const page = await Page ({ params : Promise.resolve ({ id : '1' }) });
render ( page );
expect ( screen . getByText ( 'Alice' )). toBeInTheDocument ();
});
});
```
## Anti-patterns
- ❌ `'use client'` on every component — most components can be server components
- ❌ Data fetching in client components — prefer server components for data
- ❌ `useEffect` for data fetching — use server components or TanStack Query
- ❌ Importing server-only code in client components (DB, tokens, env)
- ❌ Large client bundles — lazy load heavy components with `next/dynamic`
- ❌ Not using `notFound()` — always handle missing data
- ❌ `router.push` for navigation that should use `<Link>` prefetch