Estrutura de pastas
As camadas são um conceito. Pasta é onde o conceito encosta no
disco — e é onde a maioria dos apps escorrega, porque a estrutura mais óbvia
(components/, hooks/, services/, types/) é a que envelhece pior.
Por tipo de arquivo não escala
Comece com a estrutura que todo tutorial mostra:
src/
├── components/ ← 84 arquivos
├── hooks/ ← 31 arquivos
├── services/ ← 22 arquivos
└── types/ ← 19 arquivos
Agora responda: quais desses arquivos formam a tela de pedidos?
Você não sabe. Pra mexer em pedidos você abre quatro pastas, cada uma com dezenas de vizinhos irrelevantes. Pra apagar pedidos você caça arquivo por arquivo e deixa três órfãos.
O sintoma é o git status
Se um PR de uma única feature toca arquivos em 4 pastas diferentes e nenhuma delas se chama como a feature, a estrutura está agrupando pela coisa errada.
O problema é que components/, hooks/ e services/ agrupam por como o
arquivo foi escrito. Você nunca precisa "de todos os hooks". Você precisa
"de tudo que é pedido".
Agrupe por feature
src/
├── main.tsx # bootstrap
├── App.tsx # providers + router
├── routes.tsx # mapa de URLs
│
├── lib/ # INFRA — existe uma vez no app
│ ├── api.ts # createApiClient
│ ├── logger.ts # createLogger
│ └── storage.ts
│
├── stores/ # estado global de cliente
│ └── auth.ts # createAuthStore
│
├── layouts/ # cascas de tela
│ └── RootLayout.tsx
│
├── components/ # UI DO APP sem domínio (raro — o SDK cobre)
│ └── MoneyInput/
│
├── features/ # ⭐ o corpo do app
│ ├── orders/
│ │ ├── index.ts # API pública da feature
│ │ ├── orders.schema.ts # zod + tipos do domínio
│ │ ├── orders.service.ts # fala com o backend
│ │ ├── use-orders.ts # hooks de query/mutation
│ │ ├── use-orders.test.ts
│ │ ├── OrderTable.tsx
│ │ ├── OrderTable.test.tsx
│ │ ├── OrderStatusBadge.tsx
│ │ └── OrderTable.module.css
│ └── customers/
│ └── …
│
└── pages/ # uma tela por rota
├── Orders.tsx
└── OrderDetail.tsx
O create-tempest-app já gera lib/, stores/, layouts/, pages/ e
routes.tsx — veja Scaffold. features/ é o que você
adiciona quando a primeira feature aparece.
Teste mora ao lado do arquivo testado
OrderTable.test.tsx fica junto de OrderTable.tsx, não numa árvore
__tests__/ paralela. Duas razões: você vê num ls se algo tem teste, e
mover a feature move os testes com ela.
index.ts: a API pública da feature
Uma feature é uma caixa. O index.ts é o que sai da caixa:
// src/features/orders/index.ts
/**
* Public surface of the orders feature. Anything not re-exported here is an
* internal detail — other features and pages must not import it directly.
*/
export { OrderTable } from "./OrderTable";
export { useOrders } from "./use-orders";
export { usePayOrder } from "./use-pay-order";
export type { Order, OrderStatus } from "./orders.schema";
E o consumo:
import { OrderTable, useOrders } from "@/features/orders";
import { OrderTable } from "@/features/orders/OrderTable";
import { buildOrderQuery } from "@/features/orders/internal/query-builder";
O segundo import amarra outra parte do app a um detalhe interno. Renomear
query-builder.ts agora é breaking change.
Duas regras de barrel, e elas parecem contraditórias
Fora da feature, importe sempre pelo index.ts — é o contrato.
Dentro da feature, importe o caminho direto (./orders.service), nunca
o próprio barrel — barrel interno cria ciclo de import e faz o Vite
reprocessar a feature inteira a cada edição.
Quando criar uma pasta de feature
Não crie features/ no primeiro dia. O gatilho é objetivo:
| Situação | Onde vai |
|---|---|
| Um componente, usado numa página só, sem serviço | fica na própria página ou em components/ |
| Dois ou mais arquivos que compartilham o mesmo tipo de domínio | cria features/<domínio>/ |
| Tem serviço (fala com backend) + componente | cria features/<domínio>/ |
| Componente sem nenhum tipo de domínio, reusado em 2+ features | components/ — ou candidato a subir pro SDK |
O nome da pasta é o domínio, não a tela: orders, não orders-page;
billing, não billing-tab.
Feature não importa o miolo de outra feature
features/billing pode importar features/orders — pelo index.ts.
Quando duas features começam a puxar detalhe uma da outra toda hora, elas
eram uma feature só, ou existe um terceiro conceito compartilhado que
ninguém extraiu ainda (aí sim: features/shared/ ou lib/).
Convenção de nomes
Nome previsível economiza mais tempo que qualquer ferramenta de busca.
| Tipo de arquivo | Padrão | Exemplo |
|---|---|---|
| Componente | PascalCase.tsx |
OrderTable.tsx |
| Teste | <arquivo>.test.tsx |
OrderTable.test.tsx |
| CSS Module | <Componente>.module.css |
OrderTable.module.css |
| Hook | use-<coisa>.ts |
use-orders.ts |
| Serviço | <domínio>.service.ts |
orders.service.ts |
| Schema/tipos do domínio | <domínio>.schema.ts |
orders.schema.ts |
| Store Zustand | <domínio>.ts em stores/ |
stores/auth.ts |
| Utilitário | kebab-case.ts |
format-invoice.ts |
O identificador exportado é sempre PascalCase pra componente e
camelCase pra função — o kebab-case é só do nome de arquivo, o que evita
conflito de case em filesystem case-insensitive (macOS) contra Linux no CI.
Import com @/, sempre
O alias @ → src vem configurado pelo
createViteConfig. Use ele pra tudo que cruza pasta:
import { api } from "@/lib/api";
import { useOrders } from "@/features/orders";
import { OrderRow } from "./OrderRow";
import { api } from "../../../lib/api";
Relativo (./) só entre irmãos do mesmo diretório. ../../../ é sinal de que
você está furando uma fronteira ou de que o arquivo está na pasta errada.
tempest fix converte isso pra você
npx tempest fix --dry-run # lista o que seria reescrito
npx tempest fix # aplica: ../../../ → @/, ordena imports, remove import morto
Ordem dos imports
Três blocos, separados por linha em branco — é o que o
simple-import-sort do template já impõe:
import { useState } from "react"; // 1. externos
import { Button, DataTable } from "tempest-react-sdk";
import { api } from "@/lib/api"; // 2. do app (@/)
import { useOrders } from "@/features/orders";
import { OrderRow } from "./OrderRow"; // 3. relativos
import styles from "./OrderTable.module.css";
Não é frescura: com ordem fixa, o diff de um import novo é uma linha, não uma reorganização do bloco inteiro.
Recap
- Agrupar por tipo de arquivo não escala; agrupar por feature escala.
- Feature é caixa:
index.tsé o contrato, o resto é interno. - Dentro da feature use caminho direto; de fora use só o barrel.
- Gatilho pra criar feature: 2+ arquivos com o mesmo domínio, ou serviço + componente.
- Nome de arquivo
PascalCasepra componente,kebab-casepro resto;@/pra cruzar pasta etempest fixpra manter isso.
Próxima: Fluxo de dados — quem tem permissão de falar com o backend.