Claude
Skills
Sign in
Back

building-nextjs-apps

Included with Lifetime
$97 forever

Specialized skill for building Next.js 15 App Router applications with React Server Components, Server Actions, and production-ready patterns. Use when implementing Next.js features, components, or application structure.

Web Dev

What this skill does


# Building Next.js Apps

You are an expert in building production-ready Next.js 15 applications using the App Router with opinionated best practices.

## Enforced Patterns

### App Router Only
- NEVER use Pages Router
- Use App Router features: layouts, loading, error, not-found
- Leverage nested layouts for shared UI
- Use route groups for organization (no URL impact)

### Server Components First
Default to Server Components. Only use Client Components when you need:
- Interactivity (event handlers: onClick, onChange, etc.)
- Browser-only APIs (localStorage, window, document)
- React hooks (useState, useEffect, useReducer, etc.)
- Third-party libraries that require client-side rendering

### Data Fetching

**Server Components** (Preferred):
```typescript
// app/posts/page.tsx
import { getPosts } from '@/lib/data';

export default async function PostsPage() {
  const posts = await getPosts(); // Direct async call

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}
```

**Client Components** (When needed):
```typescript
// components/posts-list.tsx
'use client';

import { useEffect, useState } from 'react';

export function PostsList() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('/api/posts')
      .then(res => res.json())
      .then(setPosts);
  }, []);

  return <div>{/* render posts */}</div>;
}
```

### Mutations with Server Actions

**Form Actions** (Preferred):
```typescript
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1, 'Title required'),
  content: z.string().min(1, 'Content required'),
});

export async function createPost(formData: FormData) {
  const validated = CreatePostSchema.parse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  // Write to database
  const postId = await db.createPost(validated);

  revalidatePath('/posts');
  redirect(`/posts/${postId}`);
}
```

```typescript
// app/posts/new/page.tsx
import { createPost } from '@/app/actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}
```

**Programmatic Actions**:
```typescript
// components/delete-button.tsx
'use client';

import { deletePost } from '@/app/actions';

export function DeleteButton({ postId }: { postId: string }) {
  return (
    <button onClick={() => deletePost(postId)}>
      Delete
    </button>
  );
}
```

### Route Handlers

Use for external API integrations, webhooks, or when Server Actions don't fit:

```typescript
// app/api/webhook/route.ts
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';

export async function POST(request: Request) {
  const headersList = headers();
  const signature = headersList.get('x-webhook-signature');

  // Verify signature
  if (!verifySignature(signature)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const body = await request.json();

  // Process webhook
  await processWebhook(body);

  return NextResponse.json({ success: true });
}
```

### File Structure

```
app/
├── (auth)/                     # Route group (no /auth in URL)
│   ├── login/
│   │   └── page.tsx
│   ├── register/
│   │   └── page.tsx
│   └── layout.tsx              # Shared auth layout
├── (dashboard)/                # Another route group
│   ├── posts/
│   │   ├── [id]/
│   │   │   ├── page.tsx        # /posts/[id]
│   │   │   └── edit/
│   │   │       └── page.tsx    # /posts/[id]/edit
│   │   ├── new/
│   │   │   └── page.tsx        # /posts/new
│   │   ├── page.tsx            # /posts
│   │   ├── loading.tsx         # Loading UI
│   │   └── error.tsx           # Error boundary
│   ├── settings/
│   │   └── page.tsx
│   └── layout.tsx              # Dashboard layout with nav
├── api/
│   ├── webhook/
│   │   └── route.ts
│   └── health/
│       └── route.ts
├── actions.ts                  # Server Actions
├── layout.tsx                  # Root layout
├── page.tsx                    # Home page
├── loading.tsx                 # Global loading
├── error.tsx                   # Global error
├── not-found.tsx               # 404 page
└── global.css                  # Tailwind imports

components/
├── ui/                         # Reusable UI components
│   ├── button.tsx
│   ├── card.tsx
│   └── input.tsx
└── features/                   # Feature-specific components
    ├── post-card.tsx
    └── post-form.tsx

lib/
├── db/                         # Database access
│   ├── dynamodb.ts
│   └── queries.ts
├── auth/                       # Auth utilities
│   └── config.ts
└── utils.ts                    # Shared utilities
```

### Layouts

**Root Layout** (Required):
```typescript
// app/layout.tsx
import './global.css';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        {children}
      </body>
    </html>
  );
}
```

**Nested Layouts**:
```typescript
// app/(dashboard)/layout.tsx
import { Navigation } from '@/components/navigation';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen">
      <Navigation />
      <main className="flex-1 overflow-y-auto p-8">
        {children}
      </main>
    </div>
  );
}
```

### Loading States

**Streaming with Suspense**:
```typescript
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { PostsList } from '@/components/posts-list';
import { StatsSkeleton } from '@/components/skeletons';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <PostsList />
      </Suspense>
    </div>
  );
}
```

**Loading.tsx**:
```typescript
// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center h-full">
      <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900" />
    </div>
  );
}
```

### Error Handling

**Error Boundaries**:
```typescript
// app/dashboard/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="flex flex-col items-center justify-center h-full">
      <h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
      <p className="text-gray-600 mb-4">{error.message}</p>
      <button
        onClick={reset}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        Try again
      </button>
    </div>
  );
}
```

**Not Found**:
```typescript
// app/posts/[id]/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div>
      <h2>Post Not Found</h2>
      <p>Could not find the requested post.</p>
      <Link href="/posts">View all posts</Link>
    </div>
  );
}
```

### Metadata

**Static Metadata**:
```typescript
// app/posts/page.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Posts',
  description: 'Browse all posts',
};

export default function PostsPage() {
  // ...
}
```

**Dynamic Metadata**:
```typescript
// app/posts/[id]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

export async function generateMetadata({
  params,
}: {
  params: 
Files: 1
Size: 14.3 KB
Complexity: 18/100
Category: Web Dev

Related in Web Dev