nextjs
npx machina-cli add skill Squirrelfishcityhall150/claude-code-kit/nextjs --openclawNext.js Development Guidelines
Development patterns for Next.js 15+ using the App Router, Server Components, and modern data fetching.
Core Principles
- Server-First Architecture: Default to Server Components, use Client Components only when needed
- File-Based Routing: Use App Router conventions for pages, layouts, and route handlers
- Data Fetching: Fetch data where it's needed using async/await in Server Components
- Type Safety: Leverage TypeScript for route params, search params, and data types
- Performance: Optimize with streaming, parallel data fetching, and static generation
App Router Structure
File Conventions
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── posts/
│ ├── layout.tsx # Posts layout
│ ├── page.tsx # /posts
│ ├── [id]/
│ │ └── page.tsx # /posts/123
│ └── new/
│ └── page.tsx # /posts/new
└── api/
└── posts/
└── route.ts # API route handler
Page Component
// app/posts/page.tsx
import { getPosts } from '@/lib/api';
export const metadata = {
title: 'Posts',
description: 'Browse all blog posts'
};
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
</div>
);
}
Dynamic Routes
// app/posts/[id]/page.tsx
import { getPost } from '@/lib/api';
import { notFound } from 'next/navigation';
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}
export default async function PostPage({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Server vs Client Components
Server Components (Default)
Use for:
- Data fetching
- Accessing backend resources
- Keeping sensitive info on server
- Reducing client-side JavaScript
// app/posts/page.tsx (Server Component by default)
import { db } from '@/lib/db';
export default async function PostsPage() {
// Direct database access
const posts = await db.post.findMany();
return <PostList posts={posts} />;
}
Client Components
Use for:
- Event listeners (onClick, onChange, etc.)
- State and lifecycle (useState, useEffect)
- Browser-only APIs
- Custom hooks
// components/SearchBar.tsx
'use client'; // Required directive
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export function SearchBar() {
const [query, setQuery] = useState('');
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
router.push(`/search?q=${query}`);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search posts..."
/>
<button type="submit">Search</button>
</form>
);
}
Composition Pattern
// app/posts/page.tsx (Server Component)
import { getPosts } from '@/lib/api';
import { SearchBar } from '@/components/SearchBar'; // Client Component
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<SearchBar /> {/* Client Component for interactivity */}
<PostList posts={posts} /> {/* Can be Server Component */}
</div>
);
}
Data Fetching
Basic Pattern
// Server Component with async/await
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json());
return <PostList posts={posts} />;
}
Parallel Data Fetching
export default async function DashboardPage() {
// Fetch in parallel
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<Stats data={stats} />
</div>
);
}
Streaming with Suspense
import { Suspense } from 'react';
export default function PostsPage() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
async function Posts() {
const posts = await getPosts(); // Slow data fetch
return <PostList posts={posts} />;
}
Layouts
Root Layout (Required)
// app/layout.tsx
import './globals.css';
export const metadata = {
title: {
default: 'My Blog',
template: '%s | My Blog'
}
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}
Nested Layout
// app/posts/layout.tsx
export default function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<aside>
<PostsSidebar />
</aside>
<div>{children}</div>
</div>
);
}
Route Handlers (API Routes)
Basic Handler
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const posts = await getPosts();
return NextResponse.json({ posts });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json({ post }, { status: 201 });
}
Dynamic Route Handler
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id);
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json({ post });
}
Server Actions
Basic Server Action
// app/actions/posts.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const post = await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
}
Using in Forms
import { createPost } from '@/app/actions/posts';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}
Navigation
Link Component
import Link from 'next/link';
export function PostCard({ post }: { post: Post }) {
return (
<Link href={`/posts/${post.id}`} prefetch={true}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</Link>
);
}
Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
export function PostActions({ postId }: { postId: string }) {
const router = useRouter();
const handleDelete = async () => {
await deletePost(postId);
router.push('/posts');
router.refresh(); // Refresh server components
};
return <button onClick={handleDelete}>Delete</button>;
}
Router Hooks
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
const pathname = usePathname(); // /posts/123
const searchParams = useSearchParams(); // ?q=hello
const query = searchParams.get('q');
Metadata
// Static metadata
export const metadata = {
title: 'All Posts',
description: 'Browse our collection of blog posts'
};
// Dynamic metadata
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt
};
}
Error Handling
// app/posts/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/posts/[id]/not-found.tsx
export default function NotFound() {
return <div>Post Not Found</div>;
}
Static Generation & ISR
// Generate static pages at build time
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ id: post.id }));
}
// Revalidate every hour (ISR)
export const revalidate = 3600;
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const post = await getPost(id);
return <Post data={post} />;
}
Additional Resources
For detailed information, see:
Source
git clone https://github.com/Squirrelfishcityhall150/claude-code-kit/blob/main/cli/kits/nextjs/skills/nextjs/SKILL.mdView on GitHub Overview
Guidelines for building Next.js 15+ apps with the App Router, Server Components, and modern data-fetching. It promotes a server-first approach, TypeScript safety for route params and data, and performance wins through streaming, parallel fetches, and static generation. The material covers file based routing, layout patterns, error handling, loading states, and API route conventions.
How This Skill Works
Usage centers on the app directory with conventions like layout.tsx, page.tsx, loading.tsx, and error.tsx. Server Components are the default, enabling data fetching on the server; Client Components are opt-in via the use client directive for interactivity. Data is fetched with async/await inside server components, keeping secrets on the server and minimizing client side JavaScript.
When to Use It
- Building blog or CMS style pages with server side data in app/posts
- Rendering dynamic routes like /posts/[id] with server fetched data
- Exposing API endpoints under app/api/posts/route.ts
- Optimizing performance with streaming and static generation
- Enforcing strict TypeScript typings for route params and data shapes
Quick Start
- Step 1: Create or upgrade to a Next.js 15+ project and enable the App Router by using the app directory
- Step 2: Implement a server component that fetches data with async/await (eg app/posts/page.tsx)
- Step 3: Add a client component with use client for interactivity and wire routes with next/navigation
Best Practices
- Default to Server Components and convert to Client Components only when interactivity is needed
- Follow App Router file structure and create layout.tsx, page.tsx, loading.tsx, and error.tsx
- Fetch data in server components using async/await and keep backend access on server
- Use TypeScript to type route params, search params, and API data
- Leverage streaming, parallel data fetching, and static generation for performance
Example Use Cases
- Posts homepage fetching all posts via a server component
- Dynamic post page at /posts/[id] using generateMetadata for title and description
- Client search bar component using use client and next/navigation router
- API endpoint at app/api/posts/route.ts serving JSON data
- Custom not-found and loading UI with app/not-found.tsx and app/loading.tsx