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.
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.
