Pular para o conteúdo
desenvolvimento

Next.js App Router: Arquitetura de Aplicações Fullstack em 2025

Como estruturar aplicações Next.js com App Router: Server Components, Server Actions, streaming, cache e organização de projeto para escala.

Douglas M. Pereira4 min de leitura
nextjsreactfullstackarquiteturatypescript

App Router mudou o que significa desenvolver em Next.js

O App Router, estável desde Next.js 13, não foi apenas uma mudança de pasta. Foi uma mudança de paradigma: React Server Components trouxeram renderização no servidor como padrão, Server Actions eliminaram a necessidade de routes de API para mutações simples e o sistema de cache granular mudou a forma de pensar performance.

Em 2025, com Next.js 16, esse modelo está maduro e é a base para aplicações fullstack sérias.

Estrutura de pastas que escala

app/
├── (auth)/              # Route group sem segmento na URL
│   ├── login/
│   └── register/
├── (dashboard)/
│   ├── layout.tsx       # Layout compartilhado pelo dashboard
│   ├── page.tsx
│   ├── settings/
│   └── [workspace]/
├── api/
│   ├── webhooks/        # Apenas para externos (Stripe, etc.)
│   └── cron/            # Jobs periódicos
├── actions/             # Server Actions globais
│   └── user.ts
├── error.tsx            # Error boundary global
└── layout.tsx

components/
├── ui/                  # Primitivos (Button, Input, Modal)
├── shared/              # Composables (Navbar, Footer, SearchBar)
└── features/
    ├── auth/
    ├── dashboard/
    └── checkout/

lib/
├── db/                  # Acesso a banco (queries, schemas)
├── auth/                # Configuração de autenticação
└── utils/               # Utilitários genéricos

Server Components vs. Client Components: a regra prática

// Server Component (padrão no App Router): sem 'use client'
// Pode: buscar dados, acessar banco, usar segredos
// Não pode: useState, useEffect, event listeners

// app/dashboard/page.tsx
export default async function DashboardPage() {
  // Acesso direto ao banco — sem API em meio do caminho
  const orders = await db.query.orders.findMany({
    where: eq(orders.userId, (await auth()).userId),
    limit: 20,
    orderBy: desc(orders.createdAt),
  })
  
  return <OrderList orders={orders} />
}
// Client Component: necessário apenas para interatividade
'use client'

import { useState } from 'react'

// app/dashboard/components/OrderFilter.tsx
export function OrderFilter({ onFilter }: { onFilter: (status: string) => void }) {
  const [selected, setSelected] = useState('all')
  
  const handleChange = (status: string) => {
    setSelected(status)
    onFilter(status)
  }
  
  return <select value={selected} onChange={(e) => handleChange(e.target.value)} />
}

Regra: comece como Server Component. Adicione 'use client' apenas quando precisar de interatividade, estado ou hooks.

Server Actions para mutações

// app/actions/orders.ts
'use server'

import { revalidatePath } from 'next/cache'
import { z } from 'zod'
import { auth } from '@/lib/auth'

const createOrderSchema = z.object({
  productId: z.string().uuid(),
  quantity: z.number().int().positive(),
})

export async function createOrder(formData: FormData) {
  const session = await auth()
  if (!session) throw new Error('Unauthorized')
  
  const data = createOrderSchema.parse({
    productId: formData.get('productId'),
    quantity: Number(formData.get('quantity')),
  })
  
  await db.insert(orders).values({
    userId: session.user.id,
    productId: data.productId,
    quantity: data.quantity,
    status: 'pending',
  })
  
  revalidatePath('/dashboard/orders')
}
// Uso no componente
export function CreateOrderForm() {
  return (
    <form action={createOrder}>
      <input name="productId" />
      <input name="quantity" type="number" />
      <button type="submit">Criar pedido</button>
    </form>
  )
}

Streaming para páginas com dados lentos

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

export default function ReportsPage() {
  return (
    <div>
      <h1>Relatórios</h1>
      
      {/* Dados rápidos: renderiza imediatamente */}
      <QuickStats />
      
      {/* Dados lentos: mostra skeleton enquanto carrega */}
      <Suspense fallback={<ReportSkeleton />}>
        <HeavyReportChart />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <DataTable />
      </Suspense>
    </div>
  )
}

O usuário vê a página parcialmente renderizada imediatamente. Seções pesadas aparecem conforme ficam prontas — sem bloquear o resto.

Cache granular

// Cache de dados com revalidação
const products = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 }  // revalida a cada hora
})

// Cache por tag — revalidação seletiva
const posts = await fetch('https://cms.example.com/posts', {
  next: { tags: ['posts'] }
})

// Em um Server Action após mutação:
revalidateTag('posts')  // invalida apenas o cache de posts

// Sem cache (sempre fresco)
const user = await fetch('/api/user', { cache: 'no-store' })

Middleware para autenticação e redirects

// middleware.ts
import { auth } from '@/lib/auth'
import { NextResponse } from 'next/server'

export default auth((req) => {
  const isAuthenticated = !!req.auth
  const isAuthPage = req.nextUrl.pathname.startsWith('/login')
  
  if (!isAuthenticated && !isAuthPage) {
    return NextResponse.redirect(new URL('/login', req.url))
  }
  
  if (isAuthenticated && isAuthPage) {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }
})

export const config = {
  matcher: ['/((?!api|_next|images|favicon.ico).*)']
}

Conclusão

Next.js App Router em 2025 é uma plataforma fullstack completa. Server Components eliminam round-trips desnecessários, Server Actions simplificam mutações, streaming melhora percepção de performance. A curva de aprendizado é real, mas o resultado é uma aplicação mais rápida, mais segura e mais simples de manter.