nextjs-15

prowler-cloud's avatarfrom prowler-cloud

Next.js 15 App Router patterns. Trigger: When working with Next.js - routing, Server Actions, data fetching.

⭐12.5kstars🔀1.9kforks📁View on GitHub🕐Updated Jan 10, 2026

When & Why to Use This Skill

This Claude skill provides optimized patterns and best practices for Next.js 15 App Router development. It streamlines the creation of modern web applications by offering ready-to-use templates for Server Components, Server Actions, and advanced routing, ensuring developers adhere to the latest framework standards for performance and scalability.

Use Cases

  • Case 1: Implementing secure Server Actions for form handling and data mutations with automatic cache revalidation.
  • Case 2: Structuring complex application layouts using Next.js 15 file conventions, including route groups and private folders.
  • Case 3: Optimizing data fetching performance through parallel requests and streaming with React Suspense.
  • Case 4: Configuring dynamic SEO metadata and middleware for robust authentication and request handling.
  • Case 5: Enforcing 'server-only' boundaries to protect sensitive logic from being leaked to the client-side.
namenextjs-15
description>
TriggerWhen working in Next.js App Router (app/), Server Components vs Client Components, Server Actions, Route Handlers, caching/revalidation, and streaming/Suspense.
licenseApache-2.0
authorprowler-cloud
version"1.0"
scope[root, ui]
auto_invoke"App Router / Server Actions"
allowed-toolsRead, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task

App Router File Conventions

app/
├── layout.tsx          # Root layout (required)
├── page.tsx            # Home page (/)
├── loading.tsx         # Loading UI (Suspense)
├── error.tsx           # Error boundary
├── not-found.tsx       # 404 page
├── (auth)/             # Route group (no URL impact)
│   ├── login/page.tsx  # /login
│   └── signup/page.tsx # /signup
├── api/
│   └── route.ts        # API handler
└── _components/        # Private folder (not routed)

Server Components (Default)

// No directive needed - async by default
export default async function Page() {
  const data = await db.query();
  return <Component data={data} />;
}

Server Actions

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

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

  await db.users.create({ data: { name } });

  revalidatePath("/users");
  redirect("/users");
}

// Usage
<form action={createUser}>
  <input name="name" required />
  <button type="submit">Create</button>
</form>

Data Fetching

// Parallel
async function Page() {
  const [users, posts] = await Promise.all([
    getUsers(),
    getPosts(),
  ]);
  return <Dashboard users={users} posts={posts} />;
}

// Streaming with Suspense
<Suspense fallback={<Loading />}>
  <SlowComponent />
</Suspense>

Route Handlers (API)

// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

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

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

Middleware

// middleware.ts (root level)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("token");

  if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

Metadata

// Static
export const metadata = {
  title: "My App",
  description: "Description",
};

// Dynamic
export async function generateMetadata({ params }) {
  const product = await getProduct(params.id);
  return { title: product.name };
}

server-only Package

import "server-only";

// This will error if imported in client component
export async function getSecretData() {
  return db.secrets.findMany();
}