Pular para o conteúdo principal

Arquitetura em Camadas do Frontend

O frontend do BITIO é organizado como um prédio comercial onde cada andar tem uma função específica. Esta organização torna o código mais fácil de entender, testar e modificar.

Por que Organizamos em Camadas?

Imagine se uma empresa misturasse todas as funções:

  • Recepcionista fazendo contabilidade ❌
  • Contador atendendo telefone ❌
  • Gerente limpando banheiro ❌

O resultado seria caos total! No código acontece a mesma coisa - cada parte deve ter sua responsabilidade específica.

Benefícios da Organização em Camadas

  • Fácil de encontrar: Cada funcionalidade tem seu lugar
  • Fácil de testar: Cada camada pode ser testada isoladamente
  • Fácil de modificar: Mudança em uma camada não quebra outras
  • Fácil de crescer: Novas funcionalidades seguem o mesmo padrão

Visão Geral: Os 5 Andares do Prédio

🏢 PRÉDIO FRONTEND BITIO

5º Andar - SCHEMAS/TYPES │ 📋 "Contratos e Regras"
│ Define como os dados devem ser
────────────────────────────┼─────────────────────────────
4º Andar - API LAYER │ 📡 "Comunicação Externa"
│ Conversa com servidor
────────────────────────────┼─────────────────────────────
3º Andar - HOOKS │ 🎣 "Gerentes de Estado"
│ Controla dados e lógica
────────────────────────────┼─────────────────────────────
2º Andar - COMPONENTS │ 🎨 "Interface Visual"
│ Botões, formulários, listas
────────────────────────────┼─────────────────────────────
1º Andar - PAGES/ROUTES │ 🚪 "Portarias e Entrada"
│ Organiza e direciona usuários

Fluxo de Dados (Como a Informação Circula)

Usuário clica botão → Page → Component → Hook → API → Servidor → Banco
↓ ↓ ↓ ↓
Layout → Botões → Estado → Validação

1º Andar: Pages/Routes (A Portaria)

Função: Receber usuários e direcioná-los para o lugar certo

Como Funciona

Imagine a recepção de um shopping:

  • Pessoa chega perguntando "Onde fica o restaurante ABC?"
  • Recepcionista direciona: "2º andar, corredor B"
  • Não prepara a comida - apenas organiza o fluxo

Estrutura no Código

src/app/
├── (publico)/ # Área pública (clientes)
│ └── [slug]/
│ ├── page.tsx # Página do cardápio
│ ├── layout.tsx # Visual da área pública
│ └── loading.tsx # "Carregando..."
└── (admin)/ # Área restrita (funcionários)
└── dashboard/
├── page.tsx # Painel administrativo
└── layout.tsx # Visual da área admin

Exemplo Prático: Página de Cardápio

// Esta é a "recepcionista" para a página do cardápio
export default function PaginaCardapio({ params }: { params: { slug: string } }) {
// ✅ Só organiza - não busca dados nem renderiza interface
return <ComponenteCardapio nomeRestaurante={params.slug} />;
}

// ✅ Configuração para SEO (Google encontrar melhor)
export async function generateMetadata({ params }) {
const restaurante = await buscarRestaurantePorNome(params.slug);

return {
title: `${restaurante.nome} - Cardápio Digital`,
description: restaurante.descricao,
};
}

Regras desta Camada:

  • Apenas organização - direciona usuário para component certo
  • SEO e Performance - configura como Google vê a página
  • Layout visual - define estrutura geral da tela
  • Buscar dados - deixa isso para camadas específicas
  • Mostrar interface - deixa isso para os components

2º Andar: Components (A Interface Visual)

Função: Criar a interface que o usuário vê e interage

Como Funciona

É como a vitrine de uma loja:

  • Mostra produtos de forma atrativa
  • Tem botões para o cliente interagir
  • Reage às ações do usuário (cliques, digitação)
  • Não decide preços nem estoque - apenas exibe

Estrutura por Funcionalidade

src/features/cardapio/ # Tudo relacionado ao cardápio
├── components/ # Interface visual
│ ├── grade-produtos.tsx # Grade com todos os produtos
│ ├── filtro-categorias.tsx # Botões para filtrar por categoria
│ └── cabecalho-restaurante.tsx # Header com info do restaurant
├── hooks/ # (Próximo andar ↓)
├── api/ # (Próximo andar ↓)
└── schemas/ # (Próximo andar ↓)

Exemplo Prático: Grade de Produtos

// Component que mostra produtos em uma grade bonita
export function GradeProdutos({
produtos,
categoriaSelecionada,
aoAdicionarCarrinho
}) {
// ✅ Lógica simples de interface (filtrar por categoria)
const produtosFiltrados = categoriaSelecionada
? produtos.filter(produto => produto.categoria === categoriaSelecionada)
: produtos;

// ✅ Diferentes estados visuais
if (produtosFiltrados.length === 0) {
return (
<div className="text-center py-12">
<p className="text-gray-500">Nenhum produto encontrado</p>
</div>
);
}

// ✅ Interface responsiva e bonita
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
{produtosFiltrados.map((produto) => (
<CartaoProduto
key={produto.id}
nome={produto.nome}
preco={produto.preco}
imagem={produto.imagem}
aoClicarAdicionar={() => aoAdicionarCarrinho(produto.id)}
/>
))}
</div>
);
}

Tipos de Components

TipoFunçãoExemplo
ApresentaçãoSó mostrar dadosLista de produtos
InteraçãoReagir a cliquesBotão "Adicionar ao carrinho"
FormulárioCapturar dados do usuárioCadastro de produto
LayoutOrganizar outros componentsHeader, sidebar, footer

Regras desta Camada:

  • Interface + Interação - o que o usuário vê e clica
  • Estados visuais - carregando, vazio, erro
  • Design consistente - usar componentes do design system
  • Buscar dados - deixar isso para os hooks
  • Regras de negócio - focar apenas na interface

3º Andar: Hooks (Os Gerentes de Estado)

Função: Controlar dados e lógica da aplicação

Como Funciona

É como um gerente de loja:

  • Sabe quando pedir produtos do estoque (buscar dados)
  • Controla o que está em promoção (estado da aplicação)
  • Decide quando fazer pedidos para fornecedor (chamadas para API)
  • Informa vendedores sobre mudanças (notifica components)

Estrutura dos Hooks

src/features/cardapio/hooks/
├── index.ts # Exporta tudo de forma organizada
├── use-cardapio.ts # Buscar dados do cardápio
├── use-gerar-cupom.ts # Gerar cupom de desconto
└── use-rastrear-visualizacao.ts # Acompanhar o que cliente vê

Exemplo Prático: Hook do Cardápio

// Hook que gerencia os dados do cardápio
export function useCardapio(nomeRestaurante: string) {
return useQuery({
// Identificação única desta consulta
queryKey: ["cardapio", nomeRestaurante],

// Como buscar os dados
queryFn: () => buscarCardapioPorNome(nomeRestaurante),

// ✅ Configurações específicas do negócio
staleTime: 10 * 60 * 1000, // 10min - cardápio não muda frequentemente
enabled: !!nomeRestaurante, // Só busca se tem nome

// ✅ Tratamento de erro personalizado
retry: (tentativas, erro) => {
if (erro?.status === 404) return false; // Restaurant não existe
return tentativas < 2; // Tenta no máximo 2 vezes
},
});
}

// Hook para ações (ex: gerar cupom)
export function useGerarCupom(nomeRestaurante: string) {
const queryClient = useQueryClient();

return useMutation({
// Como executar a ação
mutationFn: (dadosFormulario) =>
gerarCupom(nomeRestaurante, dadosFormulario),

// ✅ O que fazer após sucesso
onSuccess: (cupom) => {
// Atualizar cache para mostrar novo cupom
queryClient.invalidateQueries({ queryKey: ["cupons"] });

// Registrar evento para analytics
rastrearEvento("cupom_gerado", {
restaurante: nomeRestaurante,
cupomId: cupom.id,
});
},
});
}

Como os Components Usam Hooks

// Component usa hook e recebe tudo "mastigado"
function PaginaCardapio({ nomeRestaurante }) {
const {
data: cardapio,
isLoading: carregando,
error: erro
} = useCardapio(nomeRestaurante);

// ✅ Hook cuida da complexidade, component só exibe
if (carregando) return <EsqueletoCarregamento />;
if (erro || !cardapio) return <ErroCardapio />;

return (
<div>
<CabecalhoRestaurante restaurante={cardapio.restaurante} />
<GradeProdutos produtos={cardapio.produtos} />
</div>
);
}

Regras desta Camada:

  • Gerenciar estado - controlar dados e cache
  • Lógica de interface - transformações, validações
  • Tratamento de erro - decidir como reagir a problemas
  • Analytics - registrar eventos importantes
  • Interface visual - deixar isso para components
  • HTTP direto - deixar isso para API layer

4º Andar: API Layer (A Comunicação Externa)

Função: Conversar com o servidor de forma segura e organizada

Como Funciona

É como o departamento de compras da empresa:

  • Faz pedidos para fornecedores (servidor)
  • Confere se mercadoria chegou correta (validação)
  • Trata problemas de entrega (erros de rede)
  • Organiza comunicação externa

Estrutura da API

src/features/cardapio/api/
├── index.ts # Exporta todas funções
├── buscar-cardapio.ts # GET /cardapio/{nome}
├── gerar-cupom.ts # POST /cupons/{nome}
└── rastrear-visualizacao.ts # POST /analytics/cardapio

Exemplo Prático: Buscar Cardápio

// Função que busca cardápio do servidor
export async function buscarCardapioPorNome(nomeRestaurante: string) {
try {
// ✅ Cliente HTTP centralizado (configurado uma vez)
const dados = await clienteAPI.get(`/cardapio/${nomeRestaurante}`);

// ✅ SEMPRE validar dados que vêm do servidor
return esquemaCardapio.parse(dados);

} catch (erro) {
// ✅ Tratamento de erro padronizado
console.error("Erro ao buscar cardápio:", erro);
throw new Error(`Falha ao buscar cardápio do restaurante: ${nomeRestaurante}`);
}
}

// Função para ações (criar, editar, deletar)
export async function gerarCupom(
nomeRestaurante: string,
dadosFormulario: DadosCupom
) {
try {
// ✅ Dados já validados pelo hook/component que chamou
const dados = await clienteAPI.post(`/cupons/${nomeRestaurante}`, dadosFormulario);

// ✅ Validar resposta do servidor
return esquemaCupom.parse(dados);

} catch (erro) {
console.error("Erro ao gerar cupom:", erro);
throw new Error("Falha ao gerar cupom");
}
}

Vantagens desta Organização

VantagemExplicaçãoExemplo
Validação AutomáticaSempre confere se dados estão corretosServidor manda preço como texto → erro detectado
Erros PadronizadosTodos erros tratados da mesma formaRede fora → mensagem amigável
Type SafetyTypeScript previne bugsTentar acessar campo inexistente → erro em tempo de desenvolvimento
ReutilizaçãoMesma função usada em vários lugaresbuscarCardapio() usado tanto no hook quanto em relatórios

Regras desta Camada:

  • Sempre validar respostas do servidor com Zod
  • Tratamento de erro padronizado e claro
  • Type safety completa (entrada e saída tipadas)
  • Cliente centralizado para configuração única
  • Lógica de interface - isso fica nos hooks
  • Estado da aplicação - isso fica nos components/hooks

5º Andar: Schemas/Types (As Regras e Contratos)

Função: Definir como os dados devem ser e garantir que estejam corretos

Como Funciona

É como o departamento jurídico da empresa:

  • Define contratos claros (estrutura dos dados)
  • Verifica se tudo está conforme regras (validação)
  • Previne problemas legais (bugs de tipo)
  • Garante que todos sigam os mesmos padrões

Estrutura dos Schemas

src/features/cardapio/schemas/
├── index.ts # Exporta tudo + types
├── restaurante.ts # Regras para dados do restaurante
├── produto.ts # Regras para dados dos produtos
└── cupom.ts # Regras para cupons de desconto

Exemplo Prático: Schema de Produto

// Definimos exatamente como um produto deve ser
export const esquemaProduto = z.object({
id: z.string().uuid(),
nome: z.string().min(1, "Nome é obrigatório").max(255, "Nome muito longo"),
descricao: z.string().nullable().optional(),
preco: z.number().positive("Preço deve ser positivo"),
imagemUrl: z.string().url().nullable().optional(),
categoriaId: z.string().uuid(),
estaDisponivel: z.boolean(),

// ✅ Valores padrão quando não informados
eVegetariano: z.boolean().optional().default(false),
eApimentado: z.boolean().optional().default(false),

// ✅ Datas como texto (padrão JSON)
criadoEm: z.string().datetime(),
atualizadoEm: z.string().datetime(),
});

// ✅ Schema para formulários (apenas alguns campos)
export const esquemaFormularioProduto = esquemaProduto.pick({
nome: true,
descricao: true,
preco: true,
categoriaId: true,
eVegetariano: true,
eApimentado: true,
});

// ✅ Schema para filtros de busca
export const esquemaFiltrosProduto = z.object({
categoriaId: z.string().uuid().optional(),
eVegetariano: z.boolean().optional(),
eApimentado: z.boolean().optional(),
precoMinimo: z.number().positive().optional(),
precoMaximo: z.number().positive().optional(),
});

// ✅ TypeScript automaticamente cria os tipos
export type Produto = z.infer<typeof esquemaProduto>;
export type DadosFormularioProduto = z.infer<typeof esquemaFormularioProduto>;
export type FiltrosProduto = z.infer<typeof esquemaFiltrosProduto>;

Na Prática: Validação Inteligente

// ❌ Dados inválidos chegando do servidor
const dadosRuins = {
nome: "", // vazio
preco: -10, // negativo
categoriaId: "abc123" // não é UUID válido
};

// ✅ Schema detecta problemas automaticamente
try {
const produto = esquemaProduto.parse(dadosRuins);
} catch (erro) {
// Recebe lista detalhada de todos os problemas:
// - "Nome é obrigatório"
// - "Preço deve ser positivo"
// - "categoriaId deve ser UUID válido"
}

Vantagens dos Schemas

VantagemO que PrevineExemplo
Validação AutomáticaDados corrompidosPreço como texto vira erro
Documentação VivaCódigo sem explicaçãoSchema mostra exatamente que campos existem
Refatoração SeguraQuebrar código ao mudarMudar nome do campo → TypeScript avisa todos os lugares
Mensagens ClarasErros técnicos confusos"UUID inválido" em vez de "Parse error line 23"

Regras desta Camada:

  • Zod schemas - fonte única de verdade para estrutura
  • Type inference - usar z.infer em vez de duplicar types
  • Composição - usar .pick(), .omit(), .extend() para reutilizar
  • Mensagens claras - erros fáceis de entender
  • Lógica de negócio - apenas estrutura de dados

Exemplo Completo: Fluxo de Buscar Cardápio

Vamos ver como todas as camadas trabalham juntas quando um cliente quer ver um cardápio:

// 1º ANDAR - PAGE: "Porteiro" direciona cliente
export default function PaginaCardapio({ params }: { params: { slug: string } }) {
return <ComponenteCardapio nomeRestaurante={params.slug} />;
}

// 2º ANDAR - COMPONENT: Interface visual
function ComponenteCardapio({ nomeRestaurante }: { nomeRestaurante: string }) {
const { data: cardapio, isLoading, error } = useCardapio(nomeRestaurante); // → Hook

if (isLoading) return <EsqueletoCarregamento />;
if (error || !cardapio) return <ErroCardapio />;

return (
<div>
<CabecalhoRestaurante restaurante={cardapio.restaurante} />
<GradeProdutos produtos={cardapio.produtos} />
</div>
);
}

// 3º ANDAR - HOOK: Gerente de estado
function useCardapio(nomeRestaurante: string) {
return useQuery({
queryKey: ["cardapio", nomeRestaurante],
queryFn: () => buscarCardapioPorNome(nomeRestaurante), // → API Layer
staleTime: 10 * 60 * 1000, // Cache por 10 minutos
});
}

// 4º ANDAR - API: Comunicação externa
async function buscarCardapioPorNome(nomeRestaurante: string) {
const dados = await clienteAPI.get(`/cardapio/${nomeRestaurante}`);
return esquemaCardapio.parse(dados); // → Schema validation
}

// 5º ANDAR - SCHEMA: Contratos e regras
const esquemaCardapio = z.object({
restaurante: esquemaRestaurante,
categorias: z.array(esquemaCategoria),
produtos: z.array(esquemaProduto),
});

Resumo: Por que Esta Organização Funciona?

Vantagens da Arquitetura em Camadas

BenefícioExplicaçãoExemplo Prático
Responsabilidades ClarasCada camada tem função específicaPage = rotas, Component = interface, Hook = estado
Fácil de TestarCada camada testável isoladamentePode testar component sem servidor rodando
Fácil de ReutilizarHooks e API functions usados em vários lugaresuseCardapio() usado tanto em página quanto relatório
Fácil de ManterMudança em uma camada não quebra outrasMudar API não quebra interface
Fácil de CrescerNovas funcionalidades seguem mesmo padrãoTodo novo feature terá Page → Component → Hook → API → Schema

Como Testar Cada Camada

// ✅ Testando Component isoladamente
describe("GradeProdutos", () => {
it("deve mostrar produtos corretamente", () => {
render(<GradeProdutos produtos={produtosMock} />);
expect(screen.getAllByRole("article")).toHaveLength(2);
});
});

// ✅ Testando Hook isoladamente
describe("useCardapio", () => {
it("deve buscar cardápio com sucesso", async () => {
const { result } = renderHook(() => useCardapio("restaurante-teste"));
await waitFor(() => expect(result.current.isSuccess).toBe(true));
});
});

Como Adicionar Nova Funcionalidade

Toda nova funcionalidade no frontend segue exatamente os mesmos passos:

  1. Schema - Definir contratos de dados
  2. API - Criar funções de comunicação
  3. Hook - Gerenciar estado e lógica
  4. Component - Criar interface visual
  5. Page - Integrar na navegação

Esta consistência faz com que qualquer desenvolvedor possa:

  • Entender rapidamente como o código funciona
  • Encontrar facilmente onde fazer mudanças
  • Adicionar funcionalidades seguindo padrão conhecido
  • Fazer manutenção sem medo de quebrar outras partes

A arquitetura em camadas é a base que permite ao BITIO crescer de forma sustentável, mantendo código limpo e fácil de manter.