Fundamentos de Qualidade de Software
O glossário de referência para todos os workflows. Clean Code, SOLID, arquitetura hexagonal, TDD, pirâmide de testes, OWASP Top 10 e disciplina de janela de contexto.
Clean Code + SOLID
SOLID é um conjunto de cinco princípios de design orientado a objetos formulados por Robert C. Martin. Cada princípio resolve uma categoria específica de problema de manutenibilidade. Violações custam caro: código difícil de testar, de estender e de entender. Quando você instrui o Claude a seguir SOLID explicitamente no CLAUDE.md, o modelo aplica esses critérios sem necessidade de reforço por sessão.
S — Single Responsibility Principle
Uma classe ou função deve ter uma única razão para mudar. O sinal prático: se você precisa de "e" para descrever o que a função faz, ela viola SRP.
// Ruim — valida E persiste E envia email
async function processUserRegistration(data: unknown) {
if (!data || typeof data !== 'object') throw new Error('invalid');
const user = await db.users.create(data as any);
await mailer.send({ to: user.email, subject: 'Welcome' });
return user;
}
// Bom — cada função tem uma responsabilidade
function validateRegistrationPayload(data: unknown): RegistrationDTO {
const parsed = RegistrationSchema.safeParse(data);
if (!parsed.success) throw new ValidationError(parsed.error);
return parsed.data;
}
async function createUser(dto: RegistrationDTO): Promise<User> {
return db.users.create(dto);
}
async function sendWelcomeEmail(user: User): Promise<void> {
await mailer.send({ to: user.email, subject: 'Welcome' });
}
// Orquestrador — apenas sequencia, não conhece detalhes
async function registerUser(data: unknown): Promise<User> {
const dto = validateRegistrationPayload(data);
const user = await createUser(dto);
await sendWelcomeEmail(user);
return user;
}O — Open/Closed Principle
Entidades devem estar abertas para extensão e fechadas para modificação. Em TypeScript, estratégia e polimorfismo são os mecanismos centrais.
// Ruim — cada novo método de pagamento exige modificar a função
function processPayment(method: string, amount: number) {
if (method === 'credit') { /* ... */ }
else if (method === 'pix') { /* ... */ }
else if (method === 'boleto') { /* ... */ } // nova linha a cada método
}
// Bom — extensão via implementação de interface, zero modificação
interface PaymentStrategy {
process(amount: number): Promise<PaymentResult>;
}
class CreditCardStrategy implements PaymentStrategy {
async process(amount: number): Promise<PaymentResult> { /* ... */ }
}
class PixStrategy implements PaymentStrategy {
async process(amount: number): Promise<PaymentResult> { /* ... */ }
}
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
execute(amount: number) { return this.strategy.process(amount); }
}L — Liskov Substitution Principle
Subtipos devem ser substituíveis por seus tipos base sem alterar a corretude do programa. O violação clássica: sobrescrever um método lançando uma exceção que a classe base não lança.
// Ruim — ReadOnlyRepository não suporta o contrato completo de Repository
class Repository<T> {
async save(entity: T): Promise<T> { /* persiste */ }
async findById(id: string): Promise<T | null> { /* busca */ }
}
class ReadOnlyRepository<T> extends Repository<T> {
async save(_entity: T): Promise<T> {
throw new Error('Read-only!'); // viola LSP — quem usa Repository espera que save funcione
}
}
// Bom — hierarquia honesta, contratos separados
interface ReadRepository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
}
interface WriteRepository<T> extends ReadRepository<T> {
save(entity: T): Promise<T>;
delete(id: string): Promise<void>;
}I — Interface Segregation Principle
Clientes não devem ser forçados a depender de interfaces que não utilizam. Interfaces gordas geram acoplamento desnecessário e dificultam mocks em testes.
// Ruim — interface monolítica, implementadores carregam métodos que não usam
interface UserService {
createUser(dto: CreateUserDTO): Promise<User>;
deleteUser(id: string): Promise<void>;
sendNotification(userId: string, msg: string): Promise<void>;
generateReport(userId: string): Promise<Report>;
}
// Bom — interfaces coesas e focadas
interface UserWriter {
createUser(dto: CreateUserDTO): Promise<User>;
deleteUser(id: string): Promise<void>;
}
interface UserNotifier {
sendNotification(userId: string, message: string): Promise<void>;
}
interface UserReporter {
generateReport(userId: string): Promise<Report>;
}D — Dependency Inversion Principle
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações. Em prática: injete interfaces, nunca instancie dependências dentro das classes de negócio.
// Ruim — OrderService conhece e instancia a implementação concreta
class OrderService {
private repo = new PrismaOrderRepository(); // acoplamento direto à infra
async placeOrder(dto: OrderDTO) {
return this.repo.save(dto);
}
}
// Bom — OrderService depende da abstração, não da concreção
interface OrderRepository {
save(dto: OrderDTO): Promise<Order>;
findById(id: string): Promise<Order | null>;
}
class OrderService {
constructor(private readonly repo: OrderRepository) {} // injeção via construtor
async placeOrder(dto: OrderDTO) {
return this.repo.save(dto);
}
}
// Wiring no ponto de entrada da aplicação
const orderService = new OrderService(new PrismaOrderRepository());Diretivas SOLID no CLAUDE.md
Adicione o seguinte bloco ao seu CLAUDE.md para que o Claude aplique esses critérios automaticamente em todo o código gerado:
# CLAUDE.md — diretivas de qualidade
- Siga SOLID rigorosamente. SRP em particular: uma função = uma responsabilidade
- Nunca use nomes como data, info, result, temp, x, y
- Funções com mais de 20 linhas provavelmente violam SRP
- Se vai copiar código, prefira extrair uma função
- Interfaces sempre antes de implementações concretas
- Injeção de dependência via construtor, nunca instancie dentro da classeArquitetura Hexagonal
A arquitetura hexagonal (Ports & Adapters, Alistair Cockburn, 2005) isola o domínio de negócio de toda infraestrutura: banco de dados, HTTP, filas, SDKs externos. O resultado: domínio testável sem mocks de infra, substituição de adaptadores sem tocar regras de negócio, e deploys que variam de provedor sem reescritas.
import de Prisma, Axios, AWS SDK ou qualquer biblioteca de infraestrutura dentro de src/domain/, a arquitetura foi violada.
Arquitetura hexagonal: domain core no centro, adapters na periferia, ports como contratos
Estrutura de pastas
src/
domain/ <-- entidades, use cases, value objects
entities/
user.ts
order.ts
use-cases/
create-user.ts
place-order.ts
value-objects/
email.ts
money.ts
ports/ <-- interfaces (contratos)
repositories/
user-repository.ts
order-repository.ts
services/
payment-service.ts
notification-service.ts
adapters/ <-- implementações concretas
http/
user-controller.ts
order-controller.ts
database/
prisma-user-repository.ts
prisma-order-repository.ts
external/
stripe-payment-service.ts
sendgrid-notification-service.tsSequência de implementação
Sempre implemente nesta ordem — nunca comece pelo adapter:
- Domain: Entidades e value objects com suas invariantes. Zero dependências externas.
- Ports: Interfaces que o domínio precisa (outbound) e que o domínio expõe (inbound). Apenas TypeScript puro.
- Use Cases: Orquestram entidades e chamam ports outbound. Testáveis com mocks das interfaces.
- Adapters: Implementam os ports. Aqui entram Prisma, Axios, AWS SDK etc.
Estratégia de testes por camada
// Testes de domínio: sem mocks, sem infra — apenas lógica pura
describe('Order entity', () => {
it('should not allow negative quantity', () => {
expect(() => new Order({ quantity: -1 })).toThrow(InvalidQuantityError);
});
});
// Testes de use case: mocks apenas das interfaces (ports)
describe('PlaceOrderUseCase', () => {
it('should persist order and notify customer', async () => {
const repo = mock<OrderRepository>();
const notifier = mock<NotificationService>();
repo.save.mockResolvedValue(buildOrder());
const useCase = new PlaceOrderUseCase(repo, notifier);
await useCase.execute(buildOrderDTO());
expect(repo.save).toHaveBeenCalledOnce();
expect(notifier.notify).toHaveBeenCalledOnce();
});
});
// Testes de adapter: infra real via testcontainers
describe('PrismaOrderRepository (integration)', () => {
let pg: StartedPostgreSqlContainer;
beforeAll(async () => {
pg = await new PostgreSqlContainer().start();
process.env.DATABASE_URL = pg.getConnectionUri();
await runMigrations();
});
afterAll(() => pg.stop());
it('should persist and retrieve order by id', async () => {
const repo = new PrismaOrderRepository(prisma);
const saved = await repo.save(buildOrderDTO());
const found = await repo.findById(saved.id);
expect(found).toEqual(saved);
});
});TDD — Red, Green, Refactor
Test-Driven Development é uma disciplina de design, não de cobertura. O ciclo de três etapas força o engenheiro a pensar na interface e no comportamento antes de qualquer linha de implementação. Isso resulta em APIs mais limpas, menos acoplamento e código que já nasce testável.
Ciclo TDD: Red (teste falha) → Green (implementacao minima) → Refactor (qualidade)
Exemplo completo em TypeScript
Implementando um serviço de cálculo de frete do zero com TDD:
Passo 1 — Red: escreva o teste (ele falha, a classe nem existe)
// shipping.spec.ts
import { ShippingCalculator } from './shipping-calculator';
describe('ShippingCalculator', () => {
describe('calculate', () => {
it('should return free shipping for orders above R$ 299', () => {
const calc = new ShippingCalculator();
expect(calc.calculate({ subtotal: 300, weightKg: 1 })).toBe(0);
});
it('should charge R$ 15 per kg for orders below threshold', () => {
const calc = new ShippingCalculator();
expect(calc.calculate({ subtotal: 100, weightKg: 2 })).toBe(30);
});
it('should throw for negative weight', () => {
const calc = new ShippingCalculator();
expect(() => calc.calculate({ subtotal: 100, weightKg: -1 }))
.toThrow('Weight must be positive');
});
});
});Passo 2 — Green: implementação mínima para os testes passarem
// shipping-calculator.ts
const FREE_SHIPPING_THRESHOLD = 299;
const PRICE_PER_KG = 15;
interface ShippingInput {
subtotal: number;
weightKg: number;
}
export class ShippingCalculator {
calculate({ subtotal, weightKg }: ShippingInput): number {
if (weightKg < 0) throw new Error('Weight must be positive');
if (subtotal > FREE_SHIPPING_THRESHOLD) return 0;
return weightKg * PRICE_PER_KG;
}
}Passo 3 — Refactor: melhore sem quebrar nenhum teste
// shipping-calculator.ts (após refactor)
export class ShippingCalculator {
private static readonly FREE_THRESHOLD = 299;
private static readonly RATE_PER_KG = 15;
calculate(input: ShippingInput): number {
this.validate(input);
return this.isFreeShipping(input.subtotal)
? 0
: this.computeShippingCost(input.weightKg);
}
private validate({ weightKg }: ShippingInput): void {
if (weightKg < 0) throw new Error('Weight must be positive');
}
private isFreeShipping(subtotal: number): boolean {
return subtotal > ShippingCalculator.FREE_THRESHOLD;
}
private computeShippingCost(weightKg: number): number {
return weightKg * ShippingCalculator.RATE_PER_KG;
}
}Configuração Vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
},
include: ['src/domain/**', 'src/use-cases/**'],
},
},
});Como instruir o Claude para TDD
Implemente ShippingCalculator seguindo TDD estrito:
1. Escreva os testes PRIMEIRO em src/domain/shipping-calculator.spec.ts
2. PARE e mostre os testes — aguarde minha aprovação antes de implementar
3. Implemente o mínimo em src/domain/shipping-calculator.ts para os testes passarem
4. Refatore mantendo todos os testes verdes
5. Nunca escreva código de produção sem um teste que o justifiquePirâmide de Testes
A pirâmide de testes (Mike Cohn) define a proporção ideal entre tipos de teste: muitos testes unitários rápidos na base, menos testes de integração no meio, poucos testes E2E lentos no topo. Inverter essa pirâmide é um dos erros mais caros em projetos de software — suítes lentas, frágeis e difíceis de diagnosticar.
Pirâmide de testes: base larga de unitários, topo restrito de E2E e carga
Unit Tests com Vitest
npm install -D vitest @vitest/coverage-v8 @vitest/ui
npx vitest run # execucao CI
npx vitest # watch mode desenvolvimento
npx vitest --coverage # com relatorio de cobertura
npx vitest --ui # interface visualCobertura mínima recomendada: 80% em todo o domain core, 100% nos use cases críticos (checkout, pagamento, autenticação).
Integration Tests com Testcontainers
npm install -D @testcontainers/postgresql @testcontainers/core// user-repository.integration.spec.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { PrismaClient } from '@prisma/client';
describe('UserRepository (integration)', () => {
let container: StartedPostgreSqlContainer;
let prisma: PrismaClient;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16').start();
process.env.DATABASE_URL = container.getConnectionUri();
prisma = new PrismaClient();
await prisma.$executeRaw`CREATE TABLE users (id uuid PRIMARY KEY, email text NOT NULL)`;
}, 60_000);
afterAll(async () => {
await prisma.$disconnect();
await container.stop();
});
it('should persist and retrieve user', async () => {
const repo = new PrismaUserRepository(prisma);
const user = await repo.create({ email: 'dev@example.com' });
const found = await repo.findById(user.id);
expect(found?.email).toBe('dev@example.com');
});
});E2E com Cypress
npm install -D cypress
npx cypress open # interface interativa
npx cypress run --headless # CI (sem GUI)// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.ts',
video: false,
screenshotOnRunFailure: true,
viewportWidth: 1280,
viewportHeight: 720,
},
});
// cypress/e2e/checkout.cy.ts — foco em jornadas críticas
describe('Checkout flow', () => {
it('should complete purchase as authenticated user', () => {
cy.login('user@example.com', 'password');
cy.visit('/products/notebook-pro');
cy.get('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="checkout-btn"]').click();
cy.get('[data-testid="card-number"]').type('4111111111111111');
cy.get('[data-testid="submit-payment"]').click();
cy.get('[data-testid="order-confirmation"]').should('be.visible');
});
});Load Tests com k6
brew install k6
k6 run k6/load.js # load test padrao
k6 run k6/stress.js # stress test// k6/load.js — load test padrao (baseline de producao)
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // ramp up
{ duration: '3m', target: 50 }, // steady state
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // p95 abaixo de 500ms
http_req_failed: ['rate<0.01'], // error rate abaixo de 1%
},
};
export default function () {
const res = http.get('https://api.example.com/products');
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1);
}
// k6/stress.js — stress test (encontrar ponto de ruptura)
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 300 },
{ duration: '5m', target: 300 },
{ duration: '2m', target: 0 },
],
};Quando usar cada nivel
| Nivel | Ferramenta | Quando usar | Velocidade | Custo de manutencao |
|---|---|---|---|---|
| Unit | Vitest | Logica de dominio, value objects, utilitarios | Milissegundos | Baixo |
| Integration | Testcontainers | Adapters de banco, HTTP clients, queues | Segundos | Medio |
| E2E | Cypress | Jornadas criticas do usuario (checkout, login) | Minutos | Alto |
| Load | k6 | Pre-release em producao simulada | Minutos | Baixo |
| Stress | k6 | Encontrar ponto de ruptura, validar auto-scaling | Decadas de minutos | Baixo |
OWASP Top 10
O OWASP Top 10 é o padrão de referência para riscos de segurança em aplicações web. Cada categoria representa uma classe de vulnerabilidade com impacto mensurável em produção. Um engenheiro sênior usa esta lista como checklist de design — não apenas como referência pós-auditoria.
| ID | Vulnerabilidade | Como mitigar | Gate no workflow |
|---|---|---|---|
| A01 | Broken Access Control | Checar permissoes em todo endpoint; deny-by-default; testes de autorizacao por role | Code review obrigatorio + testes de autorizacao |
| A02 | Cryptographic Failures | TLS 1.3+; AES-256 para dados em repouso; nunca MD5/SHA1 para senhas (use bcrypt/argon2) | Config review + secrets scanner no CI |
| A03 | Injection (SQL, NoSQL, OS) | Parameterized queries sempre; ORM com prepared statements; validacao na borda com Zod/Joi | Linting com regras anti-concatenacao + SAST |
| A04 | Insecure Design | Threat modeling antes de implementar; arquitetura hexagonal com limites claros | Design review na fase de planejamento |
| A05 | Security Misconfiguration | Headers de seguranca (CSP, HSTS, X-Frame-Options); remover endpoints de debug em producao | Helmet.js + auditoria de headers pre-deploy |
| A06 | Vulnerable Components | npm audit no CI; atualizacoes automaticas via Dependabot; lock de versoes |
npm audit --audit-level=high como gate obrigatorio |
| A07 | Auth & Session Failures | Rate limiting em login; MFA para admins; tokens de sessao com expiracao e rotacao | Testes de brute force + revisao de configuracao JWT |
| A08 | Software & Data Integrity Failures | Verificar assinaturas em dependencias; CI/CD pipeline protegido; SRI para assets externos | Assinatura de commits + revisao de pipeline |
| A09 | Security Logging & Monitoring | Logar tentativas de autenticacao; alertas em anomalias; retencao de logs por 90 dias | Validacao de observabilidade pre-launch |
| A10 | Server-Side Request Forgery (SSRF) | Validar e restringir URLs fornecidas pelo usuario; allowlist de dominios externos; bloquear IPs internos | Code review em toda funcionalidade que faz HTTP outbound |
npm audit deve ser gate obrigatorio em todo CI. Um npm audit --audit-level=high que falha deve bloquear o merge. Nunca deixe vulnerabilidades high/critical em dependencias de producao.
Diretivas de seguranca no CLAUDE.md
# CLAUDE.md — seguranca
- Sempre use parameterized queries. Nunca concatene SQL.
- Nunca commite secrets. Use variaveis de ambiente.
- Valide TODA entrada do usuario na borda (sem trusts implicitos).
- npm audit deve passar sem vulnerabilidades high/critical.
- Headers de seguranca: Helmet.js em todo servidor Express/Fastify.
- Autenticacao: nunca implemente JWT do zero — use uma lib auditada (jose, jsonwebtoken).
- Senhas: bcrypt com cost factor >= 12 ou argon2id.Validacao de entrada com Zod (A03)
import { z } from 'zod';
// Schema de validacao — define o contrato na borda
const CreateUserSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(12).max(128),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
role: z.enum(['user', 'admin']).default('user'),
});
type CreateUserDTO = z.infer<typeof CreateUserSchema>;
// Controller — valida antes de tocar o dominio
app.post('/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user = await userService.create(result.data); // dados ja validados e tipados
return res.status(201).json(user);
});Context Window Discipline
A janela de contexto do Claude Code tem limite finito — e esse limite e compartilhado entre instrucoes do sistema, historico da conversa, conteudo de arquivos lidos e output gerado. Um contexto mal gerido degrada a qualidade das respostas, aumenta custo e pode causar perda silenciosa de instrucoes importantes. Discipline de contexto e uma skill operacional de primeiro nivel.
Ciclo de crescimento de contexto e quando acionar /compact
Regras de ouro
Limites recomendados por agente:
Orquestrador: max 80K tokens de contexto ativo
Subagente: contexto inicial max 60K tokens
/compact: obrigatorio ao cruzar 70K tokens
Handoffs entre agentes:
- Sempre via arquivos no sistema de arquivos
- Nunca via variaveis in-memory ou estado de sessao
- Formato recomendado: HANDOFF.md com estado atual + proximos passos
Monitoramento:
/usage — tokens consumidos na sessao atual
/compact — comprime historico mantendo contexto essencial
/clear — limpa totalmente (use apenas para nova tarefa)Calculadora rapida de tokens
Regra de estimativa rapida:
1K tokens ~ 750 palavras ~ 50 linhas de codigo
Exemplos praticos:
CLAUDE.md bem escrito: ~2K tokens
Arquivo TypeScript tipico: ~1-3K tokens
Schema Prisma completo: ~3-5K tokens
Output de npm ls --all: ~10-30K tokens (nao leia isso!)
Janela total disponivel:
claude-opus-4: 200K tokens
claude-sonnet-4: 200K tokens
Orçamento util por sessao de desenvolvimento:
Reserva para instrucoes + historico: ~20K tokens
Disponivel para codigo e contexto: ~150K tokens
Zona de alerta (/compact): acima de 70K usadosTASK.md, HANDOFF.md). Se o subagente falhar, o trabalho nao se perde — o proximo agente le o arquivo e retoma.
Estrategia de contexto por tipo de tarefa
Tarefa pequena (bug fix, util function):
- Uma sessao, contexto direto, sem /compact necessario
Tarefa media (nova feature, refactor):
- Sessao focada na feature
- /compact ao atingir 50% da janela
- Commit antes de /compact para nao perder estado
Tarefa grande (novo modulo, migracao):
- Divida em sub-tarefas com handoffs via arquivo
- Orquestrador gera PLAN.md com fases
- Cada subagente recebe contexto minimo necessario
- Estado persistido em arquivos entre fasesFrontend Design Standards
Para projetos com UI, um conjunto coerente de decisoes de design elimina bikeshedding e garante consistencia visual sem esforco. As tres pecas fundamentais para stacks Next.js/React modernas: shadcn/ui para componentes, Lucide para iconografia e Geist para tipografia.
shadcn/ui
shadcn/ui nao e uma biblioteca instalada como dependencia — e um conjunto de componentes copiados diretamente para o projeto, buildados sobre Radix UI e estilizados com Tailwind CSS. Voce e dono do codigo; atualizacoes sao explicitas, nao silenciosas.
# Inicializacao
npx shadcn@latest init
# Adicionar componentes individualmente
npx shadcn@latest add button
npx shadcn@latest add input
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add data-table
# Componentes ficam em src/components/ui/
# Personalize diretamente — voce e o dono do codigo// Uso tipico com shadcn/ui
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
export function CreateUserDialog({ onSubmit }: { onSubmit: (data: FormData) => void }) {
return (
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo usuario</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit}>
<Input name="email" type="email" placeholder="email@exemplo.com" />
<Button type="submit">Criar</Button>
</form>
</DialogContent>
</Dialog>
);
}Lucide Icons (outlined)
Lucide e a biblioteca de icones padrao para stacks com shadcn/ui. Sempre use a variante outlined (padrao). Consistencia e inegociavel: misturar Lucide com heroicons ou FontAwesome no mesmo projeto e um antipadrao visual.
npm install lucide-react// Import pattern correto — sempre named imports, nunca default
import { User, Mail, Settings, AlertTriangle, CheckCircle2 } from 'lucide-react';
// Tamanhos padrao
<User size={16} /> // inline com texto
<User size={20} /> // botoes e inputs
<User size={24} /> // headings (padrao Lucide)
<User size={32} /> // icones standalone
// Com className para Tailwind
<AlertTriangle className="h-4 w-4 text-yellow-500" />
<CheckCircle2 className="h-5 w-5 text-green-500" />Geist Fonts
Geist e a familia tipografica criada pela Vercel para interfaces de software. Geist Sans para UI geral; Geist Mono obrigatorio para blocos de codigo, terminais e qualquer dado tecnico (IDs, hashes, endpoints).
// app/layout.tsx (Next.js App Router)
import { GeistSans } from 'geist/font/sans';
import { GeistMono } from 'geist/font/mono';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR" className={`${GeistSans.variable} ${GeistMono.variable}`}>
<body className="font-sans">
{children}
</body>
</html>
);
}
// tailwind.config.ts
export default {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-geist-sans)'],
mono: ['var(--font-geist-mono)'],
},
},
},
};Dark mode por padrao
Para aplicacoes voltadas a desenvolvedores, dark mode deve ser o padrao — nao uma opcao opcional. Configure no Tailwind com a classe dark no elemento html e implemente a troca via next-themes.
npm install next-themes// app/layout.tsx
import { ThemeProvider } from 'next-themes';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="dark"
enableSystem={false}
>
{children}
</ThemeProvider>
</body>
</html>
);
}Quando invocar o skill frontend-design
skill: frontend-design para gerar um contrato visual (UI-SPEC.md) com paleta, componentes, espacamento e comportamentos definidos. Implementar sem spec visual e a principal causa de inconsistencia em projetos de longa duracao.
# Como invocar no Claude Code
skill: frontend-design
# O skill gera:
# - UI-SPEC.md com todas as decisoes de design
# - Paleta de cores com variaveis CSS
# - Mapa de componentes necessarios
# - Regras de espacamento e tipografia
# - Comportamentos de estado (hover, focus, disabled, loading)Hooks como Quality Gates
O conceito central
Hooks nao substituem o CLAUDE.md — eles o completam. O CLAUDE.md opera no plano das instrucoes: influencia o comportamento do modelo de forma probabilistica, com eficacia em torno de 90%. Hooks operam no plano do output: interceptam o resultado e bloqueiam qualquer saida que nao atenda ao criterio definido, com eficacia de 100%.
- CLAUDE.md = influencia o comportamento (probabilistico ~90%)
- Hooks = bloqueiam output invalido (deterministico 100%)
Tabela de hooks por categoria
| Hook | Trigger | Verifica | Deterministico? |
|---|---|---|---|
PostToolUse Write/Edit |
arquivos .ts/.js/.tsx | ESLint + tsc --noEmit | Sim |
PostToolUse Write/Edit |
arquivos .ts fonte | vitest run (testes devem passar) | Sim |
PostToolUse Write/Edit |
package.json | npm audit --audit-level high (OWASP A06) | Sim |
PostToolUse Write/Edit |
src/domain/** | import boundaries — domain nao importa adapters | Sim |
PostToolUse Write/Edit |
qualquer arquivo | regex de secrets (OWASP A02) | Sim |
PreToolUse Bash |
qualquer comando | bloqueia comandos destrutivos | Sim |
PostToolUse Write/Edit |
arquivos .ts/.js | vitest coverage ≥ 80% domain core | Sim |
Configuracao base — settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{"type": "command", "command": "node scripts/quality-gate.mjs \"$CLAUDE_TOOL_INPUT_FILE_PATH\""}]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "node scripts/bash-guard.mjs"}]
}
]
}
}Script quality-gate.mjs
#!/usr/bin/env node
// quality-gate.mjs — gate de qualidade pos-escrita
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { extname } from 'node:path';
const file = process.argv[2];
if (!file) process.exit(0);
const ext = extname(file);
// OWASP A02 — Secret detection em qualquer arquivo
try {
const content = readFileSync(file, 'utf8');
const secretPattern = /(?:api_key|secret_key|password|private_key|token)\s*[:=]\s*['"][^'"]{8,}/i;
if (secretPattern.test(content)) {
console.error(`[quality-gate] BLOQUEADO: possivel secret detectado em ${file}`);
process.exit(1);
}
} catch {}
// TypeScript + ESLint — Clean Code gate
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext) && !file.includes('.test.') && !file.includes('.spec.')) {
try {
execSync('npx tsc --noEmit', { stdio: 'inherit', cwd: process.cwd() });
} catch {
console.error('[quality-gate] BLOQUEADO: TypeScript errors encontrados');
process.exit(1);
}
try {
execSync(`npx eslint --max-warnings 0 "${file}"`, { stdio: 'inherit' });
} catch {
console.error('[quality-gate] BLOQUEADO: ESLint warnings/errors encontrados');
process.exit(1);
}
// TDD gate — testes devem passar apos qualquer mudanca em source
try {
execSync('npx vitest run --reporter=dot --passWithNoTests', { stdio: 'inherit' });
} catch {
console.error('[quality-gate] BLOQUEADO: testes falhando');
process.exit(1);
}
}
// OWASP A06 — Vulnerable Components
if (file.endsWith('package.json') && !file.includes('node_modules')) {
try {
execSync('npm audit --audit-level high', { stdio: 'inherit' });
} catch {
console.error('[quality-gate] BLOQUEADO: vulnerabilidades high/critical em dependencias');
process.exit(1);
}
}
console.log(`[quality-gate] OK: ${file}`);Script bash-guard.mjs
#!/usr/bin/env node
// bash-guard.mjs — bloqueia comandos bash destrutivos
const cmd = process.env.CLAUDE_TOOL_INPUT_COMMAND ?? '';
const blocked = [
{ pattern: /DROP\s+TABLE/i, reason: 'DROP TABLE detectado' },
{ pattern: /rm\s+-rf\s+\/(?!tmp)/, reason: 'rm -rf em diretorio raiz' },
{ pattern: /git\s+push\s+--force\s+(?:origin\s+)?main/i, reason: 'force push em main' },
{ pattern: /git\s+reset\s+--hard\s+HEAD/i, reason: 'git reset --hard HEAD' },
{ pattern: /TRUNCATE\s+TABLE/i, reason: 'TRUNCATE TABLE detectado' },
];
for (const { pattern, reason } of blocked) {
if (pattern.test(cmd)) {
console.error(`[bash-guard] BLOQUEADO: ${reason}`);
console.error(`[bash-guard] Comando: ${cmd}`);
process.exit(1);
}
}Check de import boundaries (arquitetura hexagonal)
#!/usr/bin/env node
// check-boundaries.mjs — enforca arquitetura hexagonal
import { readFileSync } from 'node:fs';
const file = process.argv[2];
if (!file || !file.includes('/domain/')) process.exit(0);
const content = readFileSync(file, 'utf8');
// Domain core nao pode importar de adapters ou infrastructure
const violations = [
{ pattern: /from\s+['"].*\/adapters\//, label: 'adapters' },
{ pattern: /from\s+['"].*\/infrastructure\//, label: 'infrastructure' },
{ pattern: /from\s+['"].*\/database\//, label: 'database' },
{ pattern: /from\s+['"].*\/http\//, label: 'http' },
];
for (const { pattern, label } of violations) {
if (pattern.test(content)) {
console.error(`[boundaries] BLOQUEADO: domain/ importa de ${label} em ${file}`);
console.error('[boundaries] Domain core nao pode depender de adapters/infrastructure.');
process.exit(1);
}
}O que NAO pode ser enforcado por Hooks
Hooks sao ferramentas de verificacao de output — eles inspecionam o resultado, nao o processo. Existe uma categoria de criterios de qualidade que permanece fora do alcance deterministico dos hooks:
- TDD ORDER (testes escritos antes da impl) — hooks apenas verificam que testes existem e passam, nao a ordem de criacao
- SOLID semanticamente — linter so pega sintaxe (complexidade ciclomatica, max-lines); responsabilidade conceitual exige revisao humana
- OWASP A01 (Broken Access Control) — requer analise semantica de logica de negocio, que hooks de arquivo nao conseguem realizar