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
| Tipo | Função | Exemplo |
|---|---|---|
| Apresentação | Só mostrar dados | Lista de produtos |
| Interação | Reagir a cliques | Botão "Adicionar ao carrinho" |
| Formulário | Capturar dados do usuário | Cadastro de produto |
| Layout | Organizar outros components | Header, 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
| Vantagem | Explicação | Exemplo |
|---|---|---|
| Validação Automática | Sempre confere se dados estão corretos | Servidor manda preço como texto → erro detectado |
| Erros Padronizados | Todos erros tratados da mesma forma | Rede fora → mensagem amigável |
| Type Safety | TypeScript previne bugs | Tentar acessar campo inexistente → erro em tempo de desenvolvimento |
| Reutilização | Mesma função usada em vários lugares | buscarCardapio() 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
| Vantagem | O que Previne | Exemplo |
|---|---|---|
| Validação Automática | Dados corrompidos | Preço como texto vira erro |
| Documentação Viva | Código sem explicação | Schema mostra exatamente que campos existem |
| Refatoração Segura | Quebrar código ao mudar | Mudar nome do campo → TypeScript avisa todos os lugares |
| Mensagens Claras | Erros 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.inferem 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ício | Explicação | Exemplo Prático |
|---|---|---|
| Responsabilidades Claras | Cada camada tem função específica | Page = rotas, Component = interface, Hook = estado |
| Fácil de Testar | Cada camada testável isoladamente | Pode testar component sem servidor rodando |
| Fácil de Reutilizar | Hooks e API functions usados em vários lugares | useCardapio() usado tanto em página quanto relatório |
| Fácil de Manter | Mudança em uma camada não quebra outras | Mudar API não quebra interface |
| Fácil de Crescer | Novas funcionalidades seguem mesmo padrão | Todo 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:
- Schema - Definir contratos de dados
- API - Criar funções de comunicação
- Hook - Gerenciar estado e lógica
- Component - Criar interface visual
- 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.