Estrutura do Projeto Next.js
Este documento descreve a organização do projeto sob o paradigma do App Router (Next.js 15), cobrindo route groups, layouts, estratégia de componentes, fetching de dados, Route Handlers, Middleware e configuração geral.
1. Visão Geral do App Router
O projeto utiliza Next.js 15 com App Router — o modelo de roteamento baseado em sistema de arquivos introduzido no Next.js 13 e consolidado nas versões subsequentes. Toda a lógica de rotas reside sob app/, e cada segmento de URL corresponde a um diretório com um arquivo page.tsx (ou layout.tsx, loading.tsx, error.tsx, etc.).
Principais características adotadas no projeto:
| Recurso | Uso no projeto |
|---|---|
| React Server Components (RSC) | Padrão; todos os componentes são Server por omissão |
| Client Components | Somente quando há interatividade, hooks ou acesso ao DOM |
| Route Groups | (store) e (admin) — agrupam rotas sem alterar a URL |
| Layouts aninhados | Root layout + layout da loja + layout do admin |
| Server Actions | Mutações de formulários sem API explícita |
| Streaming com Suspense | Carregamento progressivo em listagens |
| generateStaticParams | Pré-geração de PDPs em build time |
| Middleware (Edge Runtime) | Proteção de rotas admin e reescrita de subdomínio |
2. Route Groups — (store) vs (admin)
Route groups são diretórios cujo nome está entre parênteses. O Next.js os ignora na construção da URL — seu único propósito é organização e isolamento de layout.
(store) — Loja Pública
app/(store)/
- Agrupa todas as rotas acessíveis ao consumidor final.
- Possui seu próprio
layout.tsxque inclui<Header>,<Footer>,<CartDrawer>e os providers da loja (CartProvider,WishlistProvider, etc.). - A URL resultante não contém
(store)—/produtos/vela-bambué a URL real, não/(store)/produtos/vela-bambu.
(admin) — Painel Administrativo
app/(admin)/admin/
- Agrupa o painel sob
app/(admin)/admin/, de modo que a URL pública é/admin/*. - Possui layout próprio (
app/(admin)/layout.tsx) que inclui<AdminSidebar>,<Topbar>e o design system exclusivo do admin. - Completamente isolado do layout da loja — fontes, paleta de cores e componentes são distintos.
- Protegido pelo Middleware antes mesmo de chegar ao layout.
Por que dois níveis em (admin)?
O diretório (admin) é o route group (sem efeito na URL) e admin dentro dele é o segmento real da URL. Isso permite que o layout do grupo (admin) envolva apenas as rotas administrativas sem precisar de um segmento (admin) na URL.
3. Árvore de Diretórios Completa
app/
├── layout.tsx ← Root Layout (fontes, Toaster, EnvBanner, AnalyticsScripts)
├── globals.css
├── not-found.tsx
├── robots.ts
├── sitemap.ts
│
├── (store)/ ← Route group: loja pública (sem efeito na URL)
│ ├── layout.tsx ← Header + Footer + CartDrawer
│ ├── loading.tsx
│ ├── page.tsx → /
│ ├── auth/ → /auth/login, /auth/registro, /auth/recuperar-senha
│ ├── busca/ → /busca?q=...
│ ├── carrinho/ → /carrinho
│ ├── categoria/
│ │ └── [slug]/ → /categoria/:slug
│ ├── checkout/ → /checkout, /checkout/sucesso
│ ├── conta/ → /conta, /conta/pedidos, /conta/favoritos
│ ├── contato/ → /contato
│ ├── kits/ → /kits
│ ├── privacidade/ → /privacidade
│ ├── produtos/
│ │ └── [slug]/ → /produtos/:slug (PDP)
│ ├── sobre/ → /sobre
│ └── trocas/ → /trocas
│
├── (admin)/ ← Route group: admin (sem efeito na URL)
│ ├── layout.tsx ← AdminSidebar + Topbar + providers do admin
│ └── admin/
│ ├── page.tsx → /admin (dashboard)
│ ├── loading.tsx
│ ├── api-keys/ → /admin/api-keys
│ ├── avaliacoes/ → /admin/avaliacoes
│ ├── categorias/ → /admin/categorias
│ ├── clientes/
│ │ └── [id]/ → /admin/clientes/:id
│ ├── configuracoes/ → /admin/configuracoes
│ ├── cupons/ → /admin/cupons
│ ├── depoimentos/ → /admin/depoimentos
│ ├── editor/ → /admin/editor (editor visual da home)
│ ├── editor-sobre/ → /admin/editor-sobre
│ ├── estoque/
│ │ └── [productId]/ → /admin/estoque/:productId
│ ├── frete/ → /admin/frete
│ ├── kits/ → /admin/kits
│ ├── newsletter/ → /admin/newsletter
│ ├── notificacoes/ → /admin/notificacoes
│ ├── pedidos/
│ │ └── [id]/ → /admin/pedidos/:id
│ ├── produtos/
│ │ └── [id]/ → /admin/produtos/:id
│ ├── variantes/ → /admin/variantes
│ └── webhooks/ → /admin/webhooks
│
└── api/ ← Route Handlers
├── admin/ ← Rotas admin (requer role ADMIN)
├── auth/ ← Login, registro, refresh
├── cart/ ← Persistência de carrinho server-side
├── categories/
├── contact/
├── coupons/
├── kits/
├── newsletter/
├── orders/
├── payments/ ← Integração MercadoPago / Stripe
├── products/
├── shipping/ ← Cálculo de frete
├── user/ ← Perfil, wishlist, endereços
├── v1/ ← API pública versionada
└── webhooks/ ← Webhooks de estoque e pagamento
4. Layouts
Root Layout (app/layout.tsx)
O root layout é o único componente que envolve toda a aplicação — loja e admin. Ele é responsável por:
- Declarar
<html lang="pt-BR">e o<body>. - Carregar as fontes via
next/font/google:Cinzel— fonte serifada para títulos; exposta como--font-cinzel.Montserrat— fonte sem-serifa para corpo de texto; exposta como--font-montserrat.- As variáveis CSS são aplicadas ao
<body>viacinzel.variableemontserrat.variable.
- Registrar o
<Toaster>(react-hot-toast) com estilo alinhado à identidade visual. - Renderizar
<EnvBanner>— banner de ambiente não-produção, calculado no servidor para evitar hydration mismatch. - Injetar
<AnalyticsScripts>(Google Analytics + Microsoft Clarity).
Layout da Loja (app/(store)/layout.tsx)
- Envolve exclusivamente as rotas públicas.
- Inclui
<Header>,<Footer>e<CartDrawer>. - Provê os contextos necessários para interatividade do cliente (ex.:
CartProviderse aplicável).
Layout do Admin (app/(admin)/layout.tsx)
- Envolve exclusivamente as rotas administrativas.
- Inclui
<AdminSidebar>e<Topbar>. - Usa a fonte Inter (carregada localmente ou via CDN no CSS do admin) para uma UI mais densa e técnica.
- Não herda estilos da loja — paleta neutra, tipografia utilitária.
5. Server Components vs Client Components
Política do Projeto
Regra geral: todo componente é Server Component por padrão. A diretiva
"use client"só é adicionada quando estritamente necessário.
Quando usar "use client"
Um componente precisa ser Client Component apenas quando utiliza:
- Hooks de estado/efeito:
useState,useEffect,useReducer,useRef. - Hooks do Next.js client-side:
useRouter,usePathname,useSearchParams. - Event handlers do DOM:
onClick,onChange,onSubmit. - APIs do browser:
localStorage,window,navigator. - Stores Zustand:
useCartStore,useWishlistStore,useAdminUIStore. - Animações com Framer Motion ou bibliotecas que dependem do DOM.
Exemplos de Server Components
app/(store)/produtos/[slug]/page.tsx ← busca produto no banco, gera metadata
app/(store)/categoria/[slug]/page.tsx ← lista produtos filtrados por categoria
app/(admin)/admin/pedidos/[id]/page.tsx ← carrega pedido completo para exibição
components/store/product-card.tsx ← exibe dados estáticos do produto
Exemplos de Client Components
components/store/cart-drawer.tsx ← useState + useCartStore
components/store/add-to-cart-button.tsx← useCartStore + onClick
components/store/pdp-variant-section.tsx ← seleção de variante com estado local
components/store/product-filters.tsx ← filtros interativos com useSearchParams
components/admin/admin-sidebar.tsx ← useAdminUIStore + usePathname
components/admin/product-form.tsx ← formulário controlado com react-hook-form
Padrão de Composição
Quando um Server Component precisa de interatividade em apenas uma parte da tela, a árvore é estruturada assim:
ServerPage (RSC)
└── StaticContent (RSC) ← dados do banco, SEO
└── InteractiveSection (CC) ← "use client", recebe dados via props
└── ChildCC (CC) ← herda automaticamente o contexto client
6. Dados e Fetching
Fetch no Servidor
Toda busca de dados que pode ocorrer no servidor deve ocorrer em Server Components ou Route Handlers. O padrão adotado é fetch nativo com opções de cache ou acesso direto ao banco via Prisma.
unstable_cache
Usado para memorizar resultados de queries ao banco que não variam por request (ex.: categorias, configurações globais):
import { unstable_cache } from "next/cache";
const getCategories = unstable_cache(
async () => prisma.category.findMany({ where: { active: true } }),
["categories"],
{ revalidate: 3600, tags: ["categories"] }
);
generateStaticParams para PDPs
As páginas de produto (/produtos/[slug]) são pré-geradas em build time para todos os produtos ativos:
// app/(store)/produtos/[slug]/page.tsx
export async function generateStaticParams() {
const products = await prisma.product.findMany({
where: { active: true },
select: { slug: true },
});
return products.map((p) => ({ slug: p.slug }));
}
Isso transforma as PDPs em páginas estáticas (SSG), entregues via CDN sem processamento server-side por request.
Streaming com Suspense
A listagem de produtos (/produtos) usa <Suspense> para iniciar o streaming do HTML antes que a query do banco seja concluída:
// app/(store)/produtos/page.tsx
import { Suspense } from "react";
export default function ProdutosPage() {
return (
<>
<ProductFilters /> {/* renderiza imediatamente */}
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid /> {/* streamed assim que a query terminar */}
</Suspense>
</>
);
}
7. Route Handlers
Os Route Handlers residem em app/api/ e seguem as convenções do App Router (route.ts com funções exportadas GET, POST, PUT, PATCH, DELETE).
Padrão de Autenticação
Dois helpers utilitários são usados em toda a API:
requireAuth(req)— verifica o cookietoken, decodifica o JWT e retorna o payload. Lança401se inválido.requireAdmin(req)— chamarequireAuthe verificapayload.role === "ADMIN". Lança403se insuficiente.
// app/api/admin/products/route.ts
import { requireAdmin } from "@/lib/auth";
export async function GET(req: Request) {
const admin = await requireAdmin(req);
// ... lógica segura
}
Formato de Resposta
Todas as respostas seguem o padrão:
// Sucesso
return Response.json({ data: result }, { status: 200 });
// Erro de validação
return Response.json({ error: "Campo obrigatório ausente" }, { status: 422 });
// Erro interno
return Response.json({ error: "Erro interno" }, { status: 500 });
Tratamento de Erros
export async function POST(req: Request) {
try {
const body = await req.json();
// validação com zod...
const result = await prisma.product.create({ data: body });
return Response.json({ data: result }, { status: 201 });
} catch (err) {
if (err instanceof ZodError) {
return Response.json({ error: err.errors }, { status: 422 });
}
console.error(err);
return Response.json({ error: "Erro interno" }, { status: 500 });
}
}
8. Middleware
O arquivo middleware.ts roda no Edge Runtime — sem Node.js, sem acesso ao sistema de arquivos, latência mínima. Ele intercepta requests antes que cheguem ao Next.js.
O que é Protegido
| Rota | Proteção |
|---|---|
/admin/* | JWT válido + role === "ADMIN" |
/api/admin/* | JWT válido + role === "ADMIN" |
Subdomínio admin.ljvelasearomas.com.br | Idem — reescrita para /admin/* |
Fluxo de Execução
-
Fast-exit para a loja: Se o host não é o subdomínio admin e a rota não começa com
/adminou/api/admin, o middleware retornanext()imediatamente, sem nenhum processamento extra. -
Subdomínio admin: Qualquer request para
admin.ljvelasearomas.com.bré reescrito paraljvelasearomas.com.br/admin/*. A verificação JWT ocorre antes da reescrita. -
Domínio principal, rotas admin: Para
/admin/*e/api/admin/*, o cookietokené verificado comjwtVerify(bibliotecajose). Se o token for inválido ou o role não forADMIN:- Rotas de página: redireciona para
/auth/loginou/. - Rotas de API: retorna JSON
{ error: "..." }com status401ou403.
- Rotas de página: redireciona para
Configuração do Matcher
export const config = {
matcher: [
"/admin/:path*",
"/api/admin/:path*",
// Captura todas as rotas não-estáticas (para o subdomínio admin)
"/((?!_next/static|_next/image|favicon|.*\\.(?:ico|png|svg|jpg|webp)).*)",
],
};
O padrão negativo no terceiro matcher garante que assets estáticos nunca passem pelo middleware, preservando performance.
9. Configuração Next.js (next.config.ts)
Imagens
images: {
remotePatterns: [
{ protocol: "https", hostname: "ljvelasearomas.com.br" },
{ protocol: "https", hostname: "www.ljvelasearomas.com.br" },
{ protocol: "https", hostname: "beige-cassowary-235127.hostingersite.com" },
{ protocol: "https", hostname: "images.unsplash.com" },
{ protocol: "https", hostname: "storage.googleapis.com" },
{ protocol: "https", hostname: "lh3.googleusercontent.com" },
],
formats: ["image/webp"], // AVIF desabilitado — CPU intensiva em shared hosting
deviceSizes: [640, 828, 1080, 1920],
imageSizes: [64, 128, 256, 384],
}
⚠️ Nunca usar
hostname: "**". Adicione apenas domínios reais para evitar que o servidor seja usado como proxy de imagens externas.
Headers de Segurança
Aplicados globalmente em todas as rotas:
| Header | Valor |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
Strict-Transport-Security | max-age=31536000; includeSubDomains |
Assets estáticos (/_next/static/*) recebem Cache-Control: public, max-age=31536000, immutable.
optimizePackageImports
experimental: {
optimizePackageImports: ["lucide-react", "framer-motion", "recharts"],
}
Reduz o bundle JS importando apenas os módulos efetivamente usados de pacotes pesados, sem necessidade de imports individuais (import { X } from "lucide-react/dist/esm/icons/x").
Server Actions — allowedOrigins
serverActions: {
allowedOrigins: [
"localhost:3000",
"beige-cassowary-235127.hostingersite.com",
"ljvelasearomas.com.br",
"www.ljvelasearomas.com.br",
"admin.ljvelasearomas.com.br",
],
}
Lista explícita de origens autorizadas a chamar Server Actions. Qualquer origin fora dessa lista é rejeitada com 403.