Communitygithub.com

Samueladewole/claude-skills - Padrões do App Router do Next.js

Domine o Next.js 14+ com App Router, Server Components, streaming, rotas paralelas e busca de dados avançada. Use ao construir aplicações Next.js, implementar SSR/SSG ou otimizar React Server Components.

O que é claude-skills - Padrões do App Router do Next.js?

Este skill fornece um conjunto abrangente de padrões e práticas recomendadas para o Next.js 14+ com foco no App Router, Server Components e desenvolvimento full-stack moderno com React. Ele é projetado para desenvolvedores que estão construindo novas aplicações Next.js, migrando do Pages Router para o App Router ou implementando recursos avançados como streaming, rotas paralelas e Server Actions. O skill cobre tópicos essenciais como modos de renderização (Server Components, Client Components, estático, dinâmico e streaming), convenções de arquivos do App Router (layout.tsx, page.tsx, loading.tsx, error.tsx, etc.), estratégias de cache e otimização de SEO. Ele é compatível com as versões mais recentes do Next.js e oferece exemplos práticos de código TypeScript que demonstram como buscar dados em Server Components, criar componentes client-side com 'use client', implementar Server Actions para mutações seguras, configurar rotas paralelas para layouts complexos e usar Suspense para streaming progressivo de conteúdo. O objetivo é ajudar desenvolvedores a construir aplicações performáticas e escaláveis, evitando armadilhas comuns como o uso incorreto de hooks em Server Components ou a busca de dados em Client Components sem necessidade.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Samueladewole/claude-skills/tree/HEAD/nextjs-app-router-patterns

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Padrões do App Router do Next.js

Padrões abrangentes para arquitetura do App Router do Next.js 14+, Server Components e desenvolvimento full-stack moderno com React.

Quando Usar Este Skill

  • Construindo novas aplicações Next.js com App Router
  • Migrando do Pages Router para o App Router
  • Implementando Server Components e streaming
  • Configurando rotas paralelas e rotas de interceptação
  • Otimizando busca de dados e cache
  • Construindo funcionalidades full-stack com Server Actions

Conceitos Principais

1. Modos de Renderização

ModoOndeQuando Usar
Server ComponentsSomente servidorBusca de dados, computação pesada, segredos
Client ComponentsNavegadorInteratividade, hooks, APIs do navegador
EstáticoTempo de buildConteúdo que raramente muda
DinâmicoTempo de requisiçãoDados personalizados ou em tempo real
StreamingProgressivoPáginas grandes, fontes de dados lentas

2. Convenções de Arquivos

app/
├── layout.tsx       # Invólucro de UI compartilhado
├── page.tsx         # UI da rota
├── loading.tsx      # UI de carregamento (Suspense)
├── error.tsx        # Limite de erro
├── not-found.tsx    # UI 404
├── route.ts         # Endpoint de API
├── template.tsx     # Layout remontado
├── default.tsx      # Fallback para rota paralela
└── opengraph-image.tsx  # Geração de imagem OG

Início Rápido

// app/layout.tsx
import { Inter } from 'next/font/google'
import { Providers } from './providers'

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

export const metadata = {
  title: { default: 'Minha App', template: '%s | Minha App' },
  description: 'Construído com Next.js App Router',
}

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

// app/page.tsx - Server Component por padrão
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // ISR: revalidar a cada hora
  })
  return res.json()
}

export default async function HomePage() {
  const products = await getProducts()

  return (
    <main>
      <h1>Produtos</h1>
      <ProductGrid products={products} />
    </main>
  )
}

Padrões

Padrão 1: Server Components com Busca de Dados

// app/products/page.tsx
import { Suspense } from 'react'
import { ProductList, ProductListSkeleton } from '@/components/products'
import { FilterSidebar } from '@/components/filters'

interface SearchParams {
  category?: string
  sort?: 'price' | 'name' | 'date'
  page?: string
}

export default async function ProductsPage({
  searchParams,
}: {
  searchParams: Promise<SearchParams>
}) {
  const params = await searchParams

  return (
    <div className="flex gap-8">
      <FilterSidebar />
      <Suspense
        key={JSON.stringify(params)}
        fallback={<ProductListSkeleton />}
      >
        <ProductList
          category={params.category}
          sort={params.sort}
          page={Number(params.page) || 1}
        />
      </Suspense>
    </div>
  )
}

// components/products/ProductList.tsx - Server Component
async function getProducts(filters: ProductFilters) {
  const res = await fetch(
    `${process.env.API_URL}/products?${new URLSearchParams(filters)}`,
    { next: { tags: ['products'] } }
  )
  if (!res.ok) throw new Error('Falha ao buscar produtos')
  return res.json()
}

export async function ProductList({ category, sort, page }: ProductFilters) {
  const { products, totalPages } = await getProducts({ category, sort, page })

  return (
    <div>
      <div className="grid grid-cols-3 gap-4">
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
      <Pagination currentPage={page} totalPages={totalPages} />
    </div>
  )
}

Padrão 2: Client Components com 'use client'

// components/products/AddToCartButton.tsx
'use client'

import { useState, useTransition } from 'react'
import { addToCart } from '@/app/actions/cart'

export function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition()
  const [error, setError] = useState<string | null>(null)

  const handleClick = () => {
    setError(null)
    startTransition(async () => {
      const result = await addToCart(productId)
      if (result.error) {
        setError(result.error)
      }
    })
  }

  return (
    <div>
      <button
        onClick={handleClick}
        disabled={isPending}
        className="btn-primary"
      >
        {isPending ? 'Adicionando...' : 'Adicionar ao Carrinho'}
      </button>
      {error && <p className="text-red-500 text-sm">{error}</p>}
    </div>
  )
}

Padrão 3: Server Actions

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

import { revalidateTag } from "next/cache";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function addToCart(productId: string) {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("session")?.value;

  if (!sessionId) {
    redirect("/login");
  }

  try {
    await db.cart.upsert({
      where: { sessionId_productId: { sessionId, productId } },
      update: { quantity: { increment: 1 } },
      create: { sessionId, productId, quantity: 1 },
    });

    revalidateTag("cart");
    return { success: true };
  } catch (error) {
    return { error: "Falha ao adicionar item ao carrinho" };
  }
}

export async function checkout(formData: FormData) {
  const address = formData.get("address") as string;
  const payment = formData.get("payment") as string;

  // Validar
  if (!address || !payment) {
    return { error: "Campos obrigatórios ausentes" };
  }

  // Processar pedido
  const order = await processOrder({ address, payment });

  // Redirecionar para confirmação
  redirect(`/orders/${order.id}/confirmation`);
}

Padrão 4: Rotas Paralelas

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <div className="dashboard-grid">
      <main>{children}</main>
      <aside className="analytics-panel">{analytics}</aside>
      <aside className="team-panel">{team}</aside>
    </div>
  )
}

// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
  const stats = await getAnalytics()
  return <AnalyticsChart data={stats} />
}

// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
  return <ChartSkeleton />
}

// app/dashboard/@team/page.tsx
export default async function TeamSlot() {
  const members = await getTeamMembers()
  return <TeamList members={members} />
}

Padrão 5: Rotas de Interceptação (Padrão Modal)

// Estrutura de arquivos para modal de foto
// app/
// ├── @modal/
// │   ├── (.)photos/[id]/page.tsx  # Interceptar
// │   └── default.tsx
// ├── photos/
// │   └── [id]/page.tsx            # Página completa
// └── layout.tsx

// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal'
import { PhotoDetail } from '@/components/PhotoDetail'

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = await getPhoto(id)

  return (
    <Modal>
      <PhotoDetail photo={photo} />
    </Modal>
  )
}

// app/photos/[id]/page.tsx - Versão de página completa
export default async function PhotoPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const photo = await getPhoto(id)

  return (
    <div className="photo-page">
      <PhotoDetail photo={photo} />
      <RelatedPhotos photoId={id} />
    </div>
  )
}

// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode
  modal: React.ReactNode
}) {
  return (
    <html>
      <body>
        {children}
        {modal}
      </body>
    </html>
  )
}

Padrão 6: Streaming com Suspense

// app/product/[id]/page.tsx
import { Suspense } from 'react'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  // Estes dados carregam primeiro (bloqueante)
  const product = await getProduct(id)

  return (
    <div>
      {/* Renderização imediata */}
      <ProductHeader product={product} />

      {/* Transmitir avaliações */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={id} />
      </Suspense>

      {/* Transmitir recomendações */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={id} />
      </Suspense>
    </div>
  )
}

// Estes componentes buscam seus próprios dados
async function Reviews({ productId }: { productId: string }) {
  const reviews = await getReviews(productId) // API lenta
  return <ReviewList reviews={reviews} />
}

async function Recommendations({ productId }: { productId: string }) {
  const products = await getRecommendations(productId) // Baseado em ML, lento
  return <ProductCarousel products={products} />
}

Padrão 7: Manipuladores de Rota (Rotas de API)

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

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const category = searchParams.get("category");

  const products = await db.product.findMany({
    where: category ? { category } : undefined,
    take: 20,
  });

  return NextResponse.json(products);
}

export async function POST(request: NextRequest) {
  const body = await request.json();

  const product = await db.product.create({
    data: body,
  });

  return NextResponse.json(product, { status: 201 });
}

// app/api/products/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const product = await db.product.findUnique({ where: { id } });

  if (!product) {
    return NextResponse.json({ error: "Produto não encontrado" }, { status: 404 });
  }

  return NextResponse.json(product);
}

Padrão 8: Metadados e SEO

// app/products/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'

type Props = {
  params: Promise<{ slug: string }>
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const product = await getProduct(slug)

  if (!product) return {}

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [{ url: product.image, width: 1200, height: 630 }],
    },
    twitter: {
      card: 'summary_large_image',
      title: product.name,
      description: product.description,
      images: [product.image],
    },
  }
}

export async function generateStaticParams() {
  const products = await db.product.findMany({ select: { slug: true } })
  return products.map((p) => ({ slug: p.slug }))
}

export default async function ProductPage({ params }: Props) {
  const { slug } = await params
  const product = await getProduct(slug)

  if (!product) notFound()

  return <ProductDetail product={product} />
}

Estratégias de Cache

Cache de Dados

// Sem cache (sempre fresco)
fetch(url, { cache: "no-store" });

// Cache para sempre (estático)
fetch(url, { cache: "force-cache" });

// ISR - revalidar após 60 segundos
fetch(url, { next: { revalidate: 60 } });

// Invalidação baseada em tag
fetch(url, { next: { tags: ["products"] } });

// Invalidar via Server Action
("use server");
import { revalidateTag, revalidatePath } from "next/cache";

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data });
  revalidateTag("products");
  revalidatePath("/products");
}

Melhores Práticas

Faça

  • Comece com Server Components - Adicione 'use client' apenas quando necessário
  • Coloque a busca de dados junto - Busque dados onde eles são usados
  • Use limites Suspense - Habilite streaming para dados lentos
  • Aproveite rotas paralelas - Estados de carregamento independentes
  • Use Server Actions - Para mutações com aprimoramento progressivo

Não Faça

  • Não passe dados serializáveis - Limitações da fronteira Servidor → Cliente
  • Não use hooks em Server Components - Sem useState, useEffect
  • Não busque dados em Client Components - Use Server Components ou React Query
  • Não aninhe layouts em excesso - Cada layout adiciona à árvore de componentes
  • Não ignore estados de carregamento - Sempre forneça loading.tsx ou Suspense

Recursos

Individual skills in this repo

This repo contains 5 individual skills — each has its own dedicated page.

Habilidades Relacionadas