Pular para o conteúdo principal

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:

RecursoUso no projeto
React Server Components (RSC)Padrão; todos os componentes são Server por omissão
Client ComponentsSomente quando há interatividade, hooks ou acesso ao DOM
Route Groups(store) e (admin) — agrupam rotas sem alterar a URL
Layouts aninhadosRoot layout + layout da loja + layout do admin
Server ActionsMutações de formulários sem API explícita
Streaming com SuspenseCarregamento progressivo em listagens
generateStaticParamsPré-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.tsx que 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> via cinzel.variable e montserrat.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.: CartProvider se 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 cookie token, decodifica o JWT e retorna o payload. Lança 401 se inválido.
  • requireAdmin(req) — chama requireAuth e verifica payload.role === "ADMIN". Lança 403 se 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

RotaProteção
/admin/*JWT válido + role === "ADMIN"
/api/admin/*JWT válido + role === "ADMIN"
Subdomínio admin.ljvelasearomas.com.brIdem — reescrita para /admin/*

Fluxo de Execução

  1. Fast-exit para a loja: Se o host não é o subdomínio admin e a rota não começa com /admin ou /api/admin, o middleware retorna next() imediatamente, sem nenhum processamento extra.

  2. Subdomínio admin: Qualquer request para admin.ljvelasearomas.com.br é reescrito para ljvelasearomas.com.br/admin/*. A verificação JWT ocorre antes da reescrita.

  3. Domínio principal, rotas admin: Para /admin/* e /api/admin/*, o cookie token é verificado com jwtVerify (biblioteca jose). Se o token for inválido ou o role não for ADMIN:

    • Rotas de página: redireciona para /auth/login ou /.
    • Rotas de API: retorna JSON { error: "..." } com status 401 ou 403.

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:

HeaderValor
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
Strict-Transport-Securitymax-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.