03 — Estrutura de pastas
03 — Estrutura de pastas
Árvore canônica de
src/, a responsabilidade de cada pasta e o que não pode morar nela. Estrutura é contrato: se um arquivo não cabe em nenhuma pasta, o problema é o arquivo.
Árvore
projeto/
├── .husky/ hooks de git (versionado)
├── config/ um JSON por ambiente (versionado)
│ ├── dev.json
│ ├── homolog.json
│ └── prod.json
├── docs/ documentação do projeto
├── harness/ disciplina de sessão — ver 18
├── public/
│ └── config.json GERADO por setEnvironment.js — nao versionar
├── src/
│ ├── api/ fronteira com o backend
│ ├── components/
│ │ ├── common/ reutilizáveis de domínio-neutro
│ │ ├── layout/ shell da aplicação
│ │ └── <Especifico>/ componentes de uso restrito
│ ├── context/ React Context providers
│ ├── hooks/ hooks reutilizáveis entre páginas
│ ├── lib/
│ │ └── curio/ wrapper do @curio/client
│ ├── pages/ uma pasta por tela
│ ├── router/ montagem do React Router
│ ├── routes/ declaração de paths, rotas e menu
│ ├── theme/ tema MUI
│ ├── types/ tipos globais
│ ├── utils/ funções puras
│ ├── App.tsx
│ └── main.tsx
├── .env local — nao versionar
├── .env.example versionado
├── .node-version
├── index.html
├── init.sh verificação de baseline — ver 19
├── setEnvironment.js
├── tsconfig.json
└── vite.config.ts
Responsabilidade por pasta
src/api/
A fronteira com o backend. Três arquivos, plano, sem subpastas:
| Arquivo | Responsabilidade |
|---|---|
session.ts |
login(), connect(), logoutSession() — ver 05
|
queryClient.ts |
Fábrica do QueryClient com tratamento global de erro — ver 07
|
auth.ts |
Helpers de autenticação |
Não pode morar aqui: requests de uma tela específica. Esses vivem em
src/pages/<Tela>/service/, junto de quem os usa — ver 06.
src/lib/curio/
O único lugar que importa @curio/client diretamente para configurar transporte. Envolve o
SecurityManager e expõe getSessionManager().
Não pode morar aqui: regra de negócio, componente React, chamada de caso de uso específico.
src/pages/
Uma pasta por tela. Estrutura de uma tela completa:
src/pages/Fornecedor/Cadastro/
├── IncluirFornecedorPage.tsx componente da tela
├── IncluirFornecedorPage.styles.ts objetos sx / styled
├── schemas.ts schemas zod do formulário
├── index.ts export { default } from "./IncluirFornecedorPage"
└── service/
├── hooks.ts hooks de caso de uso (useCurioMutation)
└── interfaces.ts tipos de request/response do backend
Telas simples podem omitir service/ e schemas.ts. index.ts é obrigatório — é o que permite
lazy(() => import("@pages/Fornecedor/Cadastro")).
Não pode morar aqui: componente usado por mais de uma tela (vai para components/common/),
tipo usado por mais de uma tela (vai para src/types/).
src/components/
| Subpasta | Critério |
|---|---|
common/ |
Reutilizável e agnóstico de domínio (DataTable, FormField) |
layout/ |
Shell: Main, PageContainer, TabBar
|
<Especifico>/ |
Reutilizado por 2+ telas mas preso ao domínio (ActionModal) |
Regra: um componente só sai de pages/ quando a segunda tela precisar dele. Não promova por
antecipação.
Não pode morar aqui: chamada direta de caso de uso. Componentes recebem dados por props.
Exceção: componentes de layout/ podem consumir Context (useAuth, useTabs).
src/hooks/
Hooks reutilizáveis entre páginas: useCurioMutation, useUseCaseControls, useValidatedForm,
useSearcher, useDebounce, useLocalStorage, useModal.
Não pode morar aqui: hook que serve a uma única tela — esse vai em pages/<Tela>/service/hooks.ts.
src/context/
Providers de estado global de aplicação: AuthProvider, NotificationProvider, TabsContext,
CurrentTabContext.
Não pode morar aqui: estado de servidor. Isso é React Query.
src/routes/ vs src/router/
Separação deliberada:
-
routes/= dados.paths.ts(constantes de URL),routes.ts(tabela de rotas),menuTree.ts(árvore do menu). Nenhum JSX. -
router/= comportamento.AppRouter.tsxconsome os dados e monta o React Router.
Ver 10.
src/utils/
Funções puras, sem hook/Context/sessão:
| Arquivo | Conteúdo |
|---|---|
formatters.ts |
Formatação para exibição: CPF, CNPJ, telefone, CEP, moeda, data/hora, truncar texto |
validators.ts |
Validação booleana: CPF, CNPJ, e-mail, telefone, CEP, senha forte, URL |
handlers.ts |
handleOpenReport — abre o visualizador de relatório (resources.viewer do config.json, ver 04) numa aba nova |
useCaseTriggersLogger.ts |
Log de debug de useUseCaseControls — ver 21
|
validators/Cnpj.ts + validators/index.ts
|
Validador de CNPJ no formato { isValid, errorMessage } que MaskedInput.customValidate espera — ver 08
|
validators/ é uma subpasta, não um arquivo — existe porque MaskedInput (em common/) importa
validateCnpj com uma assinatura específica (ValidationResult), diferente do isValidCNPJ booleano
de validators.ts. Os dois convivem: use isValidCNPJ para checagem simples, validateCnpj quando o
consumidor for um MaskedInput.
Não pode morar aqui: qualquer coisa que use hook, Context ou toque a sessão.
src/types/
Tipos usados por mais de uma tela. Tipos de request/response de um caso de uso específico ficam em
pages/<Tela>/service/interfaces.ts.
Barrel exports (index.ts)
Cada pasta de componente e a raiz de common/, layout/ e hooks/ expõem um index.ts.
// src/components/common/index.ts
export { default as DataTable } from "./DataTable";
export { default as FormField } from "./FormField";
export type { Column } from "./DataTable";
Armadilha comum. O barrel é mantido à mão e sai de sincronia com facilidade: um componente novo é criado mas não é exportado no
index.ts. Resultado: importes inconsistentes, uns pelo barrel e outros pelo caminho completo. Ao criar um componente, atualize o barrel no mesmo commit.
Pontos de entrada
// src/main.tsx
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { ptBR } from "date-fns/locale";
import App from "./App";
import theme from "@/theme";
import { AuthProvider } from "@/context/AuthProvider";
import { NotificationProvider, useNotification } from "@/context/NotificationProvider";
import { createQueryClient } from "@/api/queryClient";
// queryClient precisa do setNotification -> so pode nascer dentro do NotificationProvider
const AppWithQueryClient: React.FC = () => {
const { setNotification } = useNotification();
const [queryClient] = useState(() => createQueryClient(setNotification));
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</AuthProvider>
</QueryClientProvider>
);
};
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ThemeProvider theme={theme}>
<CssBaseline />
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ptBR}>
<NotificationProvider>
<AppWithQueryClient />
</NotificationProvider>
</LocalizationProvider>
</ThemeProvider>
</BrowserRouter>
</React.StrictMode>
);
A ordem dos providers é obrigatória. NotificationProvider precisa envolver o QueryClientProvider
porque o queryClient recebe setNotification na construção. AuthProvider precisa estar dentro do
QueryClientProvider (usa React Query) e dentro do BrowserRouter (usa useNavigate).
// src/App.tsx
import React from "react";
import { useAuth } from "@context/AuthProvider";
import LoadingScreen from "@components/LoadingScreen";
import AppRouter from "@/router";
const App: React.FC = () => {
const { isLoading } = useAuth();
if (isLoading) return <LoadingScreen />;
return <AppRouter />;
};
export default App;
No Comments