nextjs-16-conventions

star 3

This skill should be used when working with Next.js 16+ codebases, when the user asks about "Next.js routing", "App Router", "Server Components", "Client Components", "Server Actions", "Next.js API routes", "Next.js layouts", or when EREN detects a Next.js project. Provides modern Next.js 16+ patterns and best practices.

ugurckan By ugurckan schedule Updated 1/25/2026

name: Next.js 16+ Conventions description: This skill should be used when working with Next.js 16+ codebases, when the user asks about "Next.js routing", "App Router", "Server Components", "Client Components", "Server Actions", "Next.js API routes", "Next.js layouts", or when EREN detects a Next.js project. Provides modern Next.js 16+ patterns and best practices. version: 0.1.0

Next.js 16+ Conventions

Next.js 16+ builds upon the App Router paradigm with enhanced Server Components, improved caching, and streamlined data fetching. This skill provides decision trees and patterns for working with Next.js 16+ codebases effectively.

Core Principles

Next.js 16+ emphasizes:

  • Server Components as the default (zero client JS)
  • App Router with file-based routing conventions
  • Server Actions for mutations and form handling
  • Streaming and Suspense for progressive loading
  • Built-in caching with fine-grained control

Server vs Client Components

Decision Tree: Component Type

  1. Does it use browser APIs (window, localStorage)? → Client Component
  2. Does it use React hooks (useState, useEffect)? → Client Component
  3. Does it need event handlers (onClick)? → Client Component
  4. Does it fetch data at render time? → Server Component
  5. Does it access backend resources directly? → Server Component
  6. Is it purely presentational? → Server Component (default)

Server Component (Default)

// app/users/page.tsx - Server Component (no directive needed)
import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.user.findMany();

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Client Component

// app/components/counter.tsx - Client Component
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

Composition Pattern

Server Components can import Client Components, not vice versa:

// app/dashboard/page.tsx - Server Component
import { db } from '@/lib/db';
import { InteractiveChart } from './interactive-chart'; // Client Component

export default async function DashboardPage() {
  const data = await db.analytics.getChartData();

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Pass server data to client component */}
      <InteractiveChart data={data} />
    </div>
  );
}

App Router File Conventions

Route Files

File Purpose
page.tsx Route UI (required for route to be accessible)
layout.tsx Shared UI wrapper (persists across navigation)
loading.tsx Loading UI (Suspense boundary)
error.tsx Error UI (Error boundary)
not-found.tsx 404 UI
route.ts API endpoint

Directory Structure

app/
├── layout.tsx              # Root layout
├── page.tsx                # Home page (/)
├── globals.css
├── (auth)/                 # Route group (no URL impact)
│   ├── login/
│   │   └── page.tsx        # /login
│   └── register/
│       └── page.tsx        # /register
├── dashboard/
│   ├── layout.tsx          # Dashboard layout
│   ├── page.tsx            # /dashboard
│   ├── loading.tsx         # Loading state
│   └── [teamId]/           # Dynamic segment
│       ├── page.tsx        # /dashboard/[teamId]
│       └── settings/
│           └── page.tsx    # /dashboard/[teamId]/settings
└── api/
    └── users/
        └── route.ts        # API: /api/users

Decision Tree: Route Organization

  1. Shared UI across routes? → Create layout.tsx in parent
  2. Routes with shared logic but different URLs? → Use route groups (folder)
  3. Dynamic path segment? → Use [param] folder
  4. Catch-all segments? → Use [...slug] or [[...slug]]
  5. API endpoint? → Create route.ts (not page.tsx)

Server Actions

Server Actions handle mutations directly from components.

Form Action Pattern

// app/users/actions.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  await db.user.create({
    data: { name, email }
  });

  revalidatePath('/users');
}
// app/users/create/page.tsx - Server Component
import { createUser } from '../actions';

export default function CreateUserPage() {
  return (
    <form action={createUser}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Create User</button>
    </form>
  );
}

Client-Side Server Action

// app/components/delete-button.tsx
'use client';

import { deleteUser } from '../actions';
import { useTransition } from 'react';

export function DeleteButton({ userId }: { userId: string }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(() => deleteUser(userId))}
    >
      {isPending ? 'Deleting...' : 'Delete'}
    </button>
  );
}

Decision Tree: Server Actions

  1. Simple form submission? → Use action attribute directly
  2. Need loading state? → Use useTransition in Client Component
  3. Need form validation? → Use useActionState hook
  4. Need optimistic updates? → Use useOptimistic hook
  5. Revalidate after mutation? → Call revalidatePath or revalidateTag

Data Fetching

Fetch in Server Components

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // Revalidate every hour
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  return <PostList posts={posts} />;
}

Caching Options

Option Effect
{ cache: 'force-cache' } Cache indefinitely (default)
{ cache: 'no-store' } Never cache
{ next: { revalidate: N } } Revalidate after N seconds
{ next: { tags: ['tag'] } } Tag-based revalidation

Parallel Data Fetching

// Fetch in parallel, not sequentially
export default async function DashboardPage() {
  const [users, posts, analytics] = await Promise.all([
    getUsers(),
    getPosts(),
    getAnalytics()
  ]);

  return (
    <Dashboard users={users} posts={posts} analytics={analytics} />
  );
}

Layouts and Templates

Root Layout (Required)

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My App',
  description: 'App description'
};

export default function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <header>...</header>
        <main>{children}</main>
        <footer>...</footer>
      </body>
    </html>
  );
}

Nested Layouts

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard">
      <Sidebar />
      <div className="content">{children}</div>
    </div>
  );
}

API Routes

Route Handler

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';

export async function GET() {
  const users = await db.user.findMany();
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await db.user.create({ data: body });
  return NextResponse.json(user, { status: 201 });
}

Dynamic Route Handler

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({
    where: { id: params.id }
  });

  if (!user) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }

  return NextResponse.json(user);
}

Error Handling

Error Boundary

// app/dashboard/error.tsx
'use client';

export default function Error({
  error,
  reset
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Not Found

// app/users/[id]/page.tsx
import { notFound } from 'next/navigation';

export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await getUser(params.id);

  if (!user) {
    notFound();
  }

  return <UserProfile user={user} />;
}

Streaming and Suspense

Loading UI

// app/dashboard/loading.tsx
export default function Loading() {
  return <DashboardSkeleton />;
}

Streaming with Suspense

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<ChartSkeleton />}>
        <SlowChart />
      </Suspense>
      <Suspense fallback={<TableSkeleton />}>
        <SlowTable />
      </Suspense>
    </div>
  );
}

Additional Resources

Reference Files

For detailed patterns:

  • references/caching-strategies.md - Advanced caching patterns
  • references/authentication.md - Auth patterns with Next.js
  • references/testing-patterns.md - Testing Next.js 16+ apps

Key Points

  • Server Components are the default—add 'use client' only when needed
  • Use Server Actions for mutations instead of API routes
  • Leverage streaming and Suspense for better UX
  • Use route groups to organize without affecting URLs
  • Parallel fetch data when possible
Install via CLI
npx skills add https://github.com/ugurckan/eren --skill nextjs-16-conventions
Repository Details
star Stars 3
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator