Next.js Patterns Preset
The Next.js preset enforces Next.js App Router patterns, ensuring proper separation between Server Components, Client Components, Server Actions, and API routes.
What is the Next.js Preset?
The Next.js preset enforces patterns specific to Next.js 13+ App Router:
- Server/Client Component separation preventing client code from importing server-only utilities
- Server Actions properly isolated and callable from client components
- API routes separated from UI components
- Shared utilities accessible from both server and client
- Server-only code (database, auth) protected from client imports
The core principle: Client code cannot import server-only dependencies.
When to Use This Preset
Use the Next.js preset when you have:
- Next.js 13+ application using App Router
- React Server Components (RSC) architecture
- Server Actions for mutations
- Mixed server/client rendering needs
- Need to prevent accidental server code in client bundles
Architecture Diagram
%%{init: {'theme':'base', 'themeVariables': {
'primaryColor':'#b3d9ff',
'primaryBorderColor':'#1976d2',
'secondaryColor':'#b3ffcc',
'secondaryBorderColor':'#2e7d32',
'tertiaryColor':'#ffffb3',
'tertiaryBorderColor':'#f57c00'
}}}%%
graph TB
subgraph App["π app/
App Router Pages"]
AppPages["page.tsx
Server Components"]
AppLayouts["layout.tsx
Server Components"]
AppAPI["π api/
API Routes"]
end
subgraph Components["π components/"]
ServerComp["π server/
Server Components"]
ClientComp["π client/
'use client' Components"]
end
subgraph Actions["π actions/
Server Actions"]
ServerActions["'use server' functions"]
end
subgraph Lib["π lib/"]
ServerUtils["π server/
Database, Auth, etc."]
SharedUtils["π utils/
Universal utilities"]
end
%% Allowed dependencies (green arrows)
AppPages --> ServerComp
AppPages --> ClientComp
AppPages --> ServerActions
AppPages --> ServerUtils
AppPages --> SharedUtils
AppAPI --> ServerUtils
AppAPI --> SharedUtils
ServerComp --> ServerUtils
ServerComp --> ClientComp
ServerComp --> SharedUtils
ClientComp --> ServerActions
ClientComp --> SharedUtils
ServerActions --> ServerUtils
ServerActions --> SharedUtils
style App fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style Components fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px
style Actions fill:#fff9c4,stroke:#f57c00,stroke-width:2px
style Lib fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
linkStyle default stroke:#22c55e,stroke-width:2px
Directory Structure
βββ app/ # App Routerβ βββ page.tsx # Server Component (default)β βββ layout.tsx # Server Componentβ βββ users/β β βββ page.tsxβ βββ api/ # API Routesβ βββ users/β βββ route.tsβββ components/β βββ server/ # Server Componentsβ β βββ user-list.tsxβ β βββ dashboard.tsxβ βββ client/ # Client Componentsβ βββ user-form.tsx # "use client"β βββ interactive-chart.tsxβββ actions/ # Server Actionsβ βββ user-actions.ts # "use server"β βββ product-actions.tsβββ lib/ βββ server/ # Server-only code β βββ db.ts # Database client β βββ auth.ts # Auth utilities β βββ session.ts # Session management βββ utils/ # Universal utilities βββ format.ts βββ validation.tsBoundaries Defined
The preset defines seven boundaries:
1. App Routes
- Pattern:
app/** - Tags:
app,routes - Purpose: App Router pages and layouts (Server Components by default)
- Runtime: Server
2. API Routes
- Pattern:
app/api/** - Tags:
api,server - Purpose: API route handlers
- Runtime: Server
3. Server Components
- Pattern:
components/server/** - Tags:
components,server - Purpose: React Server Components
- Runtime: Server
4. Client Components
- Pattern:
components/client/** - Tags:
components,client - Purpose: Client Components with βuse clientβ directive
- Runtime: Client (browser)
5. Server Actions
- Pattern:
actions/** - Tags:
actions,server - Purpose: Server Actions with βuse serverβ directive
- Runtime: Server
6. Server Utilities
- Pattern:
lib/server/** - Tags:
lib,server,server-utils - Purpose: Server-only code (database, auth, etc.)
- Runtime: Server
7. Shared Utilities
- Pattern:
lib/!(server)/** - Tags:
lib,shared - Purpose: Universal utilities that work on both server and client
- Runtime: Universal
Key Rules Enforced
Client Components Cannot Import Server-Only Code
The most critical rule: Client Components cannot import from lib/server/**.
// β BAD - Client Component importing server code"use client"import { db } from '@/lib/server/database'// This will cause a build error!
// β
GOOD - Client Component using Server Action// components/client/user-form.tsx"use client"import { createUser } from '@/actions/user-actions'
export function UserForm() { async function handleSubmit(formData: FormData) { await createUser(formData) // Server Action call }
return <form action={handleSubmit}>...</form>}Client Components Can Call Server Actions
Client Components can import and call Server Actions for mutations.
// β
GOOD - Server Action"use server"import { db } from '@/lib/server/database'
export async function createUser(formData: FormData) { const email = formData.get('email') as string const name = formData.get('name') as string
await db.user.create({ data: { email, name } })}// β
GOOD - Client Component calling Server Action"use client"import { createUser } from '@/actions/user-actions'import { useState } from 'react'
export function UserForm() { const [isPending, setIsPending] = useState(false)
async function handleSubmit(formData: FormData) { setIsPending(true) await createUser(formData) setIsPending(false) }
return ( <form action={handleSubmit}> <input name="email" type="email" /> <input name="name" type="text" /> <button disabled={isPending}>Create User</button> </form> )}Server Components Can Import Everything
Server Components can import server-only code, client components, and shared utilities.
// β
GOOD - Server Component using server codeimport { db } from '@/lib/server/database'import { UserCard } from '@/components/client/user-card'
export async function UserList() { const users = await db.user.findMany()
return ( <div> {users.map(user => ( <UserCard key={user.id} user={user} /> ))} </div> )}API Routes Cannot Import UI Components
API routes should contain business logic only, not UI components.
// β BAD - API route importing componentimport { UserCard } from '@/components/client/user-card'// API routes should not import UI!
// β
GOOD - API route with business logic// app/api/users/route.tsimport { db } from '@/lib/server/database'import { NextResponse } from 'next/server'
export async function GET() { const users = await db.user.findMany() return NextResponse.json(users)}Shared Utilities Are Universal
Shared utilities can be imported by both server and client code.
// β
GOOD - Shared utilityexport function formatCurrency(amount: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)}
// Used in Server Component// components/server/product-price.tsximport { formatCurrency } from '@/lib/utils/format'
export function ProductPrice({ price }: { price: number }) { return <span>{formatCurrency(price)}</span>}
// Used in Client Component// components/client/price-input.tsx"use client"import { formatCurrency } from '@/lib/utils/format'
export function PriceInput({ onChange }: Props) { const [value, setValue] = useState(0) return <span>{formatCurrency(value)}</span>}Example Configuration
{ "preset": "@stricture/nextjs"}No additional configuration needed for standard Next.js App Router structure.
Real Code Example
Server Component with Data Fetching
// app/users/page.tsx (Server Component by default)import { db } from '@/lib/server/database'import { UserCard } from '@/components/client/user-card'
export default async function UsersPage() { // Direct database access in Server Component const users = await db.user.findMany({ orderBy: { createdAt: 'desc' } })
return ( <div> <h1>Users</h1> {users.map(user => ( <UserCard key={user.id} user={user} /> ))} </div> )}Client Component with Interactivity
"use client"import { useState } from 'react'import { deleteUser } from '@/actions/user-actions'
interface Props { user: { id: string name: string email: string }}
export function UserCard({ user }: Props) { const [isDeleting, setIsDeleting] = useState(false)
async function handleDelete() { setIsDeleting(true) await deleteUser(user.id) }
return ( <div> <h3>{user.name}</h3> <p>{user.email}</p> <button onClick={handleDelete} disabled={isDeleting}> {isDeleting ? 'Deleting...' : 'Delete'} </button> </div> )}Server Actions
"use server"import { db } from '@/lib/server/database'import { revalidatePath } from 'next/cache'
export async function createUser(formData: FormData) { const email = formData.get('email') as string const name = formData.get('name') as string
await db.user.create({ data: { email, name } })
revalidatePath('/users')}
export async function deleteUser(id: string) { await db.user.delete({ where: { id } })
revalidatePath('/users')}Server-Only Utilities
import { PrismaClient } from '@prisma/client'
declare global { var prisma: PrismaClient | undefined}
export const db = global.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') { global.prisma = db}import { cookies } from 'next/headers'import { SignJWT, jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
export async function createSession(userId: string) { const token = await new SignJWT({ userId }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('24h') .sign(secret)
cookies().set('session', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 })}
export async function getSession() { const token = cookies().get('session')?.value if (!token) return null
try { const verified = await jwtVerify(token, secret) return verified.payload as { userId: string } } catch { return null }}API Route
import { db } from '@/lib/server/database'import { NextResponse } from 'next/server'
export async function GET() { const users = await db.user.findMany() return NextResponse.json(users)}
export async function POST(request: Request) { const body = await request.json()
const user = await db.user.create({ data: { email: body.email, name: body.name } })
return NextResponse.json(user, { status: 201 })}Common Violations and Fixes
Violation: Client Component importing database
// β BAD"use client"import { db } from '@/lib/server/database'// Client Components cannot import server-only code!Fix: Use Server Component or Server Action
// β
GOOD - Option 1: Server Componentimport { db } from '@/lib/server/database'
export async function UserList() { const users = await db.user.findMany() return <div>{/* render users */}</div>}
// β
GOOD - Option 2: Client Component + Server Action// components/client/user-list.tsx"use client"import { getUsers } from '@/actions/user-actions'import { useEffect, useState } from 'react'
export function UserList() { const [users, setUsers] = useState([])
useEffect(() => { getUsers().then(setUsers) }, [])
return <div>{/* render users */}</div>}Violation: API route importing component
// β BADimport { UserCard } from '@/components/client/user-card'// API routes should not import UI components!Fix: Keep API routes focused on data
// β
GOODimport { db } from '@/lib/server/database'
export async function GET() { const users = await db.user.findMany() return Response.json(users)}Benefits
- Type safety: Prevents runtime errors from server code in client bundles
- Performance: Ensures client bundles donβt include server dependencies
- Security: Protects server-only code (database credentials, auth) from exposure
- Clear boundaries: Explicit separation of server and client concerns
Trade-offs
- File organization: Need to organize components by runtime
- Learning curve: Team needs to understand Server Components vs Client Components
- Migration: Existing Next.js apps may need restructuring
Best Practices
Colocate by Feature, Separate by Runtime
features/βββ users/ βββ components/ β βββ server/ β β βββ user-list.tsx β βββ client/ β βββ user-form.tsx βββ actions/ β βββ user-actions.ts βββ page.tsxUse Server Actions for Mutations
Prefer Server Actions over API routes for form submissions and mutations.
// β
GOOD - Server Action (simpler)"use server"export async function createUser(formData: FormData) { await db.user.create({ data: { ... } })}
// β οΈ LESS IDEAL - API route (more code)export async function POST(request: Request) { const body = await request.json() await db.user.create({ data: body }) return Response.json({ success: true })}Keep Shared Utils Pure
Shared utilities should work on both server and client.
// β
GOOD - Pure functionexport function formatDate(date: Date): string { return date.toLocaleDateString()}
// β BAD - Server-only in sharedimport { cookies } from 'next/headers'export function getCookie() { ... }Related Patterns
- Modular Architecture - Can combine with Next.js preset
- Layered Architecture - Alternative organization
Next Steps
- Check out the Next.js example
- Read Next.js App Router docs
- Learn about Server Actions