#claude#claude code#llm#spec drive development#ai#ai-engineer#nextjs#typescript

Spec-Driven Development com Claude Code: Construindo Apps Next.js Prontos para Produção

Com o Claude Code, o desenvolvimento orientado a especificações torna-se 10 vezes mais eficaz.

Anderson LimaAI Engineer
February 5, 2026
53 min read
98 views
Spec-Driven Development com Claude Code: Construindo Apps Next.js Prontos para Produção

Introdução

Você passou três horas implementando uma funcionalidade. O código funciona. Então, o gerente de produto diz: "Não foi isso que eu quis dizer."

Parece familiar?

Este é o assassino número um de produtividade no desenvolvimento de software. Não são testes lentos. Não são requisitos pouco claros. Não são algoritmos complexos. É construir a coisa errada.

Os fluxos de trabalho de desenvolvimento tradicionais incentivam ir direto para o código. "Mova-se rápido e quebre as coisas." Mas existe um caminho melhor — um que assistentes de codificação de IA como o Claude Code tornam incrivelmente poderoso.

O Desenvolvimento Orientado a Especificações (Spec-Driven Development - SDD) inverte o fluxo de trabalho: você escreve a especificação primeiro, valida-a com as partes interessadas e só então escreve o código que implementa exatamente essa especificação.

Com o Claude Code, o desenvolvimento orientado a especificações torna-se 10 vezes mais eficaz. A IA pode:

  • Ajudar você a identificar lacunas em suas especificações
  • Gerar casos de teste a partir de suas especificações
  • Implementar código que segue rigorosamente a especificação
  • Validar implementações em relação aos requisitos originais

Este artigo mostra exatamente como aplicar o desenvolvimento orientado a especificações em projetos Next.js do mundo real, com comandos passo a passo do Claude Code que você pode usar hoje.


O que é Desenvolvimento Orientado a Especificações?

O Desenvolvimento Orientado a Especificações é uma metodologia onde as especificações precedem a implementação. Antes de escrever uma única linha de código, você define:

  1. O quê a funcionalidade faz (requisitos funcionais)
  2. Como ela deve se comportar (contratos de comportamento)
  3. Quando ela falha (especificações de tratamento de erros)
  4. Por que as decisões de design foram tomadas (fundamentação arquitetural)

Isso não é cascata (waterfall). Você não escreve um documento de 200 páginas. Você escreve especificações enxutas (lean) e testáveis que se tornam o contrato entre os requisitos de negócio e o código.

O Fluxo de Trabalho SDD

┌─────────────────────────────────────────────────────────────────┐
│              DESENVOLVIMENTO ORIENTADO A ESPECIFICAÇÕES          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   1. CAPTURAR         2. ESPECIFICAR      3. VALIDAR            │
│   ┌─────────┐         ┌─────────┐         ┌─────────┐          │
│   │ História│   ──▶   │ Arquivo │   ──▶   │ Casos   │          │
│   │ Usuário │         │ da Spec │         │ de Teste│          │
│   └─────────┘         └─────────┘         └─────────┘          │
│        │                   │                   │                │
│        └───────────────────┼───────────────────┘                │
│                            ▼                                    │
│                    4. IMPLEMENTAR                               │
│                    ┌─────────────┐                              │
│                    │    Código   │                              │
│                    │ (Assist. IA)│                             │
│                    └─────────────┘                              │
│                            │                                    │
│                            ▼                                    │
│                    5. VERIFICAR                                 │
│                    ┌─────────────┐                              │
│                    │Testes Passam│                              │
│                    │Spec Atendida│                              │
│                    └─────────────┘                              │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Por que SDD + IA é o Casamento Perfeito

Os assistentes de codificação de IA se destacam quando recebem instruções claras e inequívocas. As especificações são exatamente isso.

Sem uma especificação, você pede ao Claude Code: "Construa um sistema de autenticação de usuário." A IA adivinha seus requisitos, provavelmente perdendo suas necessidades específicas sobre manipulação de sessão, MFA e logs de auditoria.

Com uma especificação, você dá ao Claude Code um contrato preciso:

## Especificação do Sistema de Autenticação

### Requisitos Funcionais
- Usuários se autenticam via e-mail/senha ou OAuth (Google, GitHub)
- As sessões expiram após 24 horas de inatividade
- Tentativas de login malsucedidas têm limite de taxa: 5 tentativas a cada 15 minutos
- Todos os eventos de autenticação são registrados em uma tabela de auditoria

### Contrato da API
POST /api/auth/login
  Requisição: { email: string, password: string }
  Resposta (200): { token: string, user: User, expiresAt: ISO8601 }
  Resposta (401): { error: "invalid_credentials" }
  Resposta (429): { error: "rate_limited", retryAfter: number }

A IA agora tem tudo o que precisa para gerar o código correto logo na primeira tentativa.


Configurando seu Fluxo de Trabalho Orientado a Especificações com Claude Code

Antes de mergulhar nos exemplos, vamos estabelecer o fluxo de trabalho e o ferramental.

Estrutura do Projeto para Especificações

Crie um diretório specs/ dedicado em seu projeto Next.js:

seu-app-nextjs/
├── specs/
│   ├── features/           # Especificações de funcionalidades
│   │   ├── auth/
│   │   │   ├── login.spec.md
│   │   │   ├── register.spec.md
│   │   │   └── password-reset.spec.md
│   │   └── products/
│   │       ├── catalog.spec.md
│   │       └── checkout.spec.md
│   ├── api/                # Especificações de endpoints de API
│   │   ├── auth.api.md
│   │   └── products.api.md
│   └── components/         # Especificações de componentes de UI
│       ├── forms.spec.md
│       └── navigation.spec.md
├── src/
│   └── app/               # Next.js App Router
└── tests/
    └── specs/             # Testes gerados a partir das especificações

Configuração do Claude Code

Adicione isto ao arquivo CLAUDE.md do seu projeto:

## Fluxo de Trabalho de Desenvolvimento Orientado a Especificações (SDD)

### Leitura de Specs
Antes de implementar qualquer funcionalidade, SEMPRE leia o arquivo de especificação relevante primeiro:
- Specs de funcionalidades: `specs/features/[dominio]/[funcionalidade].spec.md`
- Specs de API: `specs/api/[dominio].api.md`
- Specs de componentes: `specs/components/[componente].spec.md`

### Regras de Implementação
1. Nunca implemente sem ler a especificação primeiro
2. Se nenhuma especificação existir, crie uma antes de codar
3. Gere testes a partir da especificação antes de implementar
4. Verifique a implementação em relação à especificação após a codificação

### Formato do Arquivo de Especificação
As especificações devem incluir:
- Requisitos Funcionais (o que faz)
- Contrato da API (entradas/saídas)
- Tratamento de Erros (modos de falha)
- Casos de Borda (condições de contorno)
- Requisitos de Desempenho (se aplicável)

Padrão 1: Especificação de Funcionalidade com Claude Code

Vamos percorrer um exemplo completo: construir uma funcionalidade de catálogo de produtos para um app de e-commerce Next.js.

Passo 1: Criar a Especificação da Funcionalidade

Primeiro, crie o arquivo de especificação com o Claude Code:

# No terminal do Claude Code
claude "Crie uma especificação de funcionalidade para um catálogo de produtos em specs/features/products/catalog.spec.md. O catálogo deve suportar:
- Listagem de produtos com paginação
- Filtragem por categoria, faixa de preço e disponibilidade
- Ordenação por preço, nome e data de adição
- Busca com correspondência difusa (fuzzy matching)
- Páginas de detalhes do produto com produtos relacionados"

O Claude Code gera:

# Especificação da Funcionalidade de Catálogo de Produtos

## Visão Geral
Um sistema de catálogo de produtos que permite aos clientes navegar, filtrar, pesquisar e visualizar produtos.

## Requisitos Funcionais

### FR-1: Listagem de Produtos
- Exibir produtos em uma grade paginada (12 produtos por página)
- Mostrar cartão do produto com: imagem, nome, preço, avaliação, status de estoque
- Suportar rolagem infinita OU botões de paginação (configurável)

### FR-2: Filtragem
Os filtros se aplicam cumulativamente (lógica AND):

| Filtro | Tipo | Comportamento |
|--------|------|---------------|
| Categoria | Seleção múltipla | Mostrar produtos em QUALQUER categoria selecionada |
| Faixa de Preço | Slider duplo | Preço mín/máx inclusive |
| Disponibilidade | Alternância (Toggle) | Apenas em estoque quando ativado |
| Avaliação | Seleção única | Limite mínimo de avaliação |

### FR-3: Ordenação
| Opção de Ordenação | Direção Padrão |
|--------------------|----------------|
| Preço | Menor para Maior |
| Nome | A a Z |
| Data de Adição | Mais Recente Primeiro |
| Popularidade | Mais Popular |
| Avaliação | Maior Primeiro |

### FR-4: Busca
- Correspondência difusa (fuzzy matching) no nome e descrição do produto
- Mínimo de 2 caracteres para disparar a busca
- Debounce: 300ms após o último toque de tecla
- Realçar termos correspondentes nos resultados
- Mostrar "Nenhum resultado" com sugestões quando vazio

### FR-5: Detalhes do Produto
- Exibição completa das informações do produto
- Galeria de imagens com zoom
- Funcionalidade de adicionar ao carrinho
- Seção de produtos relacionados (4 produtos, mesma categoria)
- Produtos visualizados recentemente (armazenados no localStorage)

## Contrato da API

### GET /api/products
Lista produtos com filtragem e paginação.

**Parâmetros de Consulta (Query Parameters):**
```typescript
interface ProductListParams {
  page?: number;           // Padrão: 1
  limit?: number;          // Padrão: 12, máx: 50
  category?: string[];     // Slugs de categoria
  minPrice?: number;       // Preço mínimo em centavos
  maxPrice?: number;       // Preço máximo em centavos
  inStock?: boolean;       // Filtrar apenas em estoque
  minRating?: number;      // 1-5
  sort?: 'price' | 'name' | 'date' | 'popularity' | 'rating';
  order?: 'asc' | 'desc';
  search?: string;         // Consulta de busca
}
```

**Resposta (200):**
```typescript
interface ProductListResponse {
  products: Product[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
    hasNext: boolean;
    hasPrev: boolean;
  };
  filters: {
    categories: CategoryCount[];
    priceRange: { min: number; max: number };
  };
}

interface Product {
  id: string;
  slug: string;
  name: string;
  description: string;
  price: number;           // Preço em centavos
  compareAtPrice?: number; // Preço original se estiver em oferta
  images: ProductImage[];
  category: Category;
  rating: number;          // 0-5
  reviewCount: number;
  inStock: boolean;
  stockQuantity: number;
  createdAt: string;       // ISO 8601
}
```

### GET /api/products/[slug]
Recupera um único produto pelo slug.

**Resposta (200):**
```typescript
interface ProductDetailResponse {
  product: Product & {
    attributes: ProductAttribute[];
    relatedProducts: Product[];
  };
}
```

**Resposta (404):**
```typescript
{ error: "product_not_found" }
```

## Tratamento de Erros

| Cenário | Resposta | Comportamento da UI |
|---------|----------|---------------------|
| Número de página inválido | 400 | Redefinir para página 1 |
| Valores de filtro inválidos | 400 | Ignorar inválidos, aplicar válidos |
| Produto não encontrado | 404 | Mostrar página "Produto não encontrado" |
| Erro do servidor | 500 | Mostrar estado de erro com botão de repetir |
| Timeout de rede | - | Mostrar dados em cache + indicador de desatualizado |

## Casos de Borda (Edge Cases)

1. **Catálogo vazio**: Mostrar "Nenhum produto disponível" com CTA de administrador
2. **Tudo filtrado**: Mostrar "Nenhum produto corresponde aos filtros" com botão de redefinir
3. **Busca sem resultados**: Mostrar sugestões baseadas em correspondências parciais
4. **Filtro de preço excede a faixa**: Limitar à faixa disponível
5. **Mudanças de filtro simultâneas**: Aplicar debounce nas chamadas de API, cancelar pendentes

## Requisitos de Desempenho

- Carga inicial: < 1.5s em conexão 3G
- Resposta de filtro/ordenação: < 300ms percebidos
- Debounce de busca: 300ms
- Carregamento tardio (lazy loading) de imagem: Intersection Observer
- Cache: 5 minutos stale-while-revalidate

## Requisitos de Acessibilidade

- Navegação por teclado para todos os filtros
- Anúncios de leitor de tela para mudanças de filtro
- Gerenciamento de foco na paginação
- Rótulos ARIA para todos os elementos interativos
- Suporte a movimento reduzido para animações

Passo 2: Gerar Testes a partir da Especificação

Agora gere os testes com base na spec:

# No Claude Code
claude "Leia specs/features/products/catalog.spec.md e gere casos de teste abrangentes. Crie:
1. Testes unitários para a rota da API em tests/api/products.test.ts
2. Testes de componente para a UI do catálogo em tests/components/ProductCatalog.test.tsx
3. Testes E2E para o fluxo completo em tests/e2e/catalog.spec.ts

Use Vitest para testes unitários/de componente e Playwright para E2E."

O Claude Code gera os arquivos de teste:

// tests/api/products.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createMocks } from 'node-mocks-http'
import { GET } from '@/app/api/products/route'

describe('GET /api/products', () => {
  describe('Paginação (FR-1)', () => {
    it('retorna 12 produtos por padrão', async () => {
      const { req } = createMocks({ method: 'GET' })
      const response = await GET(req)
      const data = await response.json()

      expect(data.products).toHaveLength(12)
      expect(data.pagination.limit).toBe(12)
    })

    it('respeita o tamanho de página personalizado até 50', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { limit: '25' }
      })
      const response = await GET(req)
      const data = await response.json()

      expect(data.products).toHaveLength(25)
    })

    it('limita o limite em 50 independentemente da requisição', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { limit: '100' }
      })
      const response = await GET(req)
      const data = await response.json()

      expect(data.pagination.limit).toBe(50)
    })

    it('retorna os metadados de paginação corretos', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { page: '2', limit: '10' }
      })
      const response = await GET(req)
      const data = await response.json()

      expect(data.pagination).toMatchObject({
        page: 2,
        limit: 10,
        hasNext: expect.any(Boolean),
        hasPrev: true
      })
    })
  })

  describe('Filtragem (FR-2)', () => {
    it('filtra por uma única categoria', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { category: 'electronics' }
      })
      const response = await GET(req)
      const data = await response.json()

      data.products.forEach((product: Product) => {
        expect(product.category.slug).toBe('electronics')
      })
    })

    it('filtra por múltiplas categorias com lógica OR', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { category: ['electronics', 'clothing'] }
      })
      const response = await GET(req)
      const data = await response.json()

      data.products.forEach((product: Product) => {
        expect(['electronics', 'clothing']).toContain(product.category.slug)
      })
    })

    it('filtra por faixa de preço inclusive', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { minPrice: '1000', maxPrice: '5000' }
      })
      const response = await GET(req)
      const data = await response.json()

      data.products.forEach((product: Product) => {
        expect(product.price).toBeGreaterThanOrEqual(1000)
        expect(product.price).toBeLessThanOrEqual(5000)
      })
    })

    it('filtra apenas em estoque quando ativado', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { inStock: 'true' }
      })
      const response = await GET(req)
      const data = await response.json()

      data.products.forEach((product: Product) => {
        expect(product.inStock).toBe(true)
      })
    })

    it('aplica múltiplos filtros com lógica AND', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: {
          category: 'electronics',
          minPrice: '1000',
          inStock: 'true'
        }
      })
      const response = await GET(req)
      const data = await response.json()

      data.products.forEach((product: Product) => {
        expect(product.category.slug).toBe('electronics')
        expect(product.price).toBeGreaterThanOrEqual(1000)
        expect(product.inStock).toBe(true)
      })
    })
  })

  describe('Ordenação (FR-3)', () => {
    it('ordena por preço ascendente por padrão', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { sort: 'price' }
      })
      const response = await GET(req)
      const data = await response.json()

      for (let i = 1; i < data.products.length; i++) {
        expect(data.products[i].price).toBeGreaterThanOrEqual(
          data.products[i - 1].price
        )
      }
    })

    it('ordena por preço descendente quando especificado', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { sort: 'price', order: 'desc' }
      })
      const response = await GET(req)
      const data = await response.json()

      for (let i = 1; i < data.products.length; i++) {
        expect(data.products[i].price).toBeLessThanOrEqual(
          data.products[i - 1].price
        )
      }
    })

    it('ordena por data de adição (mais recente primeiro) por padrão', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { sort: 'date' }
      })
      const response = await GET(req)
      const data = await response.json()

      for (let i = 1; i < data.products.length; i++) {
        expect(new Date(data.products[i].createdAt).getTime())
          .toBeLessThanOrEqual(new Date(data.products[i - 1].createdAt).getTime())
      }
    })
  })

  describe('Busca (FR-4)', () => {
    it('retorna correspondências difusas no nome do produto', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { search: 'iphon' } // Erro de digitação intencional
      })
      const response = await GET(req)
      const data = await response.json()

      expect(data.products.some((p: Product) =>
        p.name.toLowerCase().includes('iphone')
      )).toBe(true)
    })

    it('ignora busca com menos de 2 caracteres', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { search: 'a' }
      })
      const response = await GET(req)
      const data = await response.json()

      // Deve retornar resultados não filtrados
      expect(data.products.length).toBeGreaterThan(0)
    })

    it('busca tanto no nome quanto na descrição', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { search: 'wireless charging' }
      })
      const response = await GET(req)
      const data = await response.json()

      data.products.forEach((product: Product) => {
        const matchesName = product.name.toLowerCase().includes('wireless')
        const matchesDesc = product.description.toLowerCase().includes('wireless')
        expect(matchesName || matchesDesc).toBe(true)
      })
    })
  })

  describe('Tratamento de Erros', () => {
    it('retorna 400 para número de página inválido', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { page: '-1' }
      })
      const response = await GET(req)

      expect(response.status).toBe(400)
    })

    it('ignora valores de filtro inválidos mas aplica os válidos', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: {
          category: 'electronics',
          minPrice: 'not-a-number'
        }
      })
      const response = await GET(req)
      const data = await response.json()

      expect(response.status).toBe(200)
      data.products.forEach((product: Product) => {
        expect(product.category.slug).toBe('electronics')
      })
    })
  })

  describe('Casos de Borda', () => {
    it('retorna array vazio quando todos os produtos são filtrados', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { minPrice: '99999999' }
      })
      const response = await GET(req)
      const data = await response.json()

      expect(data.products).toHaveLength(0)
      expect(data.pagination.total).toBe(0)
    })

    it('limita o filtro de preço à faixa disponível', async () => {
      const { req } = createMocks({
        method: 'GET',
        query: { minPrice: '-100', maxPrice: '99999999' }
      })
      const response = await GET(req)
      const data = await response.json()

      expect(data.filters.priceRange.min).toBeGreaterThanOrEqual(0)
    })
  })
})

Passo 3: Implementar com Base na Especificação

Agora implemente a funcionalidade com o Claude Code:

# No Claude Code
claude "Implemente a funcionalidade de catálogo de produtos com base em specs/features/products/catalog.spec.md. Crie:

1. Rota da API: src/app/api/products/route.ts
2. Rota da API: src/app/api/products/[slug]/route.ts
3. Página do catálogo: src/app/products/page.tsx
4. Página de detalhes do produto: src/app/products/[slug]/page.tsx
5. Componentes: ProductCard, ProductFilters, ProductGrid, SearchBar

Use:
- Next.js 14 App Router com Server Components
- Prisma para consultas ao banco de dados
- React Query para estado no lado do cliente
- Tailwind CSS para estilização
- Shadcn/ui para componentes

Siga a spec exatamente. Não adicione funcionalidades que não estejam na spec."

O Claude Code gera a implementação:

// src/app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { z } from 'zod'
import Fuse from 'fuse.js'

const querySchema = z.object({
  page: z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(50).default(12),
  category: z.union([z.string(), z.array(z.string())]).optional(),
  minPrice: z.coerce.number().min(0).optional(),
  maxPrice: z.coerce.number().optional(),
  inStock: z.enum(['true', 'false']).optional(),
  minRating: z.coerce.number().min(1).max(5).optional(),
  sort: z.enum(['price', 'name', 'date', 'popularity', 'rating']).optional(),
  order: z.enum(['asc', 'desc']).optional(),
  search: z.string().optional(),
})

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url)
    const params = Object.fromEntries(searchParams.entries())

    // Lidar com parâmetros de array
    const categories = searchParams.getAll('category')
    if (categories.length > 0) {
      params.category = categories
    }

    const parsed = querySchema.safeParse(params)

    if (!parsed.success) {
      return NextResponse.json(
        { error: 'invalid_parameters', details: parsed.error.flatten() },
        { status: 400 }
      )
    }

    const {
      page,
      limit,
      category,
      minPrice,
      maxPrice,
      inStock,
      minRating,
      sort,
      order,
      search
    } = parsed.data

    // Construir cláusula where com base na spec FR-2
    const where: any = {}

    if (category) {
      const categories = Array.isArray(category) ? category : [category]
      where.category = { slug: { in: categories } }
    }

    if (minPrice !== undefined || maxPrice !== undefined) {
      where.price = {}
      if (minPrice !== undefined) where.price.gte = minPrice
      if (maxPrice !== undefined) where.price.lte = maxPrice
    }

    if (inStock === 'true') {
      where.inStock = true
    }

    if (minRating !== undefined) {
      where.rating = { gte: minRating }
    }

    // Construir orderBy com base na spec FR-3
    let orderBy: any = { createdAt: 'desc' } // Padrão

    if (sort) {
      const direction = order || getDefaultOrder(sort)
      switch (sort) {
        case 'price':
          orderBy = { price: direction }
          break
        case 'name':
          orderBy = { name: direction }
          break
        case 'date':
          orderBy = { createdAt: direction }
          break
        case 'popularity':
          orderBy = { salesCount: direction }
          break
        case 'rating':
          orderBy = { rating: direction }
          break
      }
    }

    // Obter total para paginação
    const total = await prisma.product.count({ where })

    // Obter produtos
    let products = await prisma.product.findMany({
      where,
      orderBy,
      skip: (page - 1) * limit,
      take: limit,
      include: {
        category: true,
        images: {
          take: 1,
          orderBy: { order: 'asc' }
        }
      }
    })

    // Lidar com busca usando fuzzy matching (FR-4)
    if (search && search.length >= 2) {
      const allProducts = await prisma.product.findMany({
        where,
        include: {
          category: true,
          images: { take: 1, orderBy: { order: 'asc' } }
        }
      })

      const fuse = new Fuse(allProducts, {
        keys: ['name', 'description'],
        threshold: 0.4, // Limite para fuzzy matching
        includeScore: true
      })

      const searchResults = fuse.search(search)
      products = searchResults
        .slice((page - 1) * limit, page * limit)
        .map(result => result.item)
    }

    // Obter metadados de filtro
    const priceRange = await prisma.product.aggregate({
      _min: { price: true },
      _max: { price: true }
    })

    const categoryCounts = await prisma.category.findMany({
      select: {
        id: true,
        name: true,
        slug: true,
        _count: { select: { products: true } }
      }
    })

    return NextResponse.json({
      products: products.map(formatProduct),
      pagination: {
        page,
        limit,
        total: search ? products.length : total,
        totalPages: Math.ceil((search ? products.length : total) / limit),
        hasNext: page * limit < (search ? products.length : total),
        hasPrev: page > 1
      },
      filters: {
        categories: categoryCounts.map(c => ({
          id: c.id,
          name: c.name,
          slug: c.slug,
          count: c._count.products
        })),
        priceRange: {
          min: priceRange._min.price || 0,
          max: priceRange._max.price || 0
        }
      }
    })
  } catch (error) {
    console.error('Erro na listagem de produtos:', error)
    return NextResponse.json(
      { error: 'internal_error' },
      { status: 500 }
    )
  }
}

function getDefaultOrder(sort: string): 'asc' | 'desc' {
  // Com base nos padrões da spec FR-3
  switch (sort) {
    case 'price': return 'asc'      // Menor para Maior
    case 'name': return 'asc'       // A a Z
    case 'date': return 'desc'      // Mais Recente Primeiro
    case 'popularity': return 'desc' // Mais Popular
    case 'rating': return 'desc'    // Maior Primeiro
    default: return 'desc'
  }
}

function formatProduct(product: any) {
  return {
    id: product.id,
    slug: product.slug,
    name: product.name,
    description: product.description,
    price: product.price,
    compareAtPrice: product.compareAtPrice,
    images: product.images,
    category: {
      id: product.category.id,
      name: product.category.name,
      slug: product.category.slug
    },
    rating: product.rating,
    reviewCount: product.reviewCount,
    inStock: product.inStock,
    stockQuantity: product.stockQuantity,
    createdAt: product.createdAt.toISOString()
  }
}
// src/app/products/page.tsx
import { Suspense } from 'react'
import { ProductGrid } from '@/components/products/ProductGrid'
import { ProductFilters } from '@/components/products/ProductFilters'
import { SearchBar } from '@/components/products/SearchBar'
import { ProductGridSkeleton } from '@/components/products/ProductGridSkeleton'

interface ProductsPageProps {
  searchParams: {
    page?: string
    category?: string | string[]
    minPrice?: string
    maxPrice?: string
    inStock?: string
    sort?: string
    order?: string
    search?: string
  }
}

export default async function ProductsPage({ searchParams }: ProductsPageProps) {
  return (
    <div className="container mx-auto px-4 py-8">
      <div className="flex flex-col lg:flex-row gap-8">
        {/* Barra Lateral de Filtros - FR-2 */}
        <aside className="w-full lg:w-64 flex-shrink-0">
          <Suspense fallback={<div className="animate-pulse h-96 bg-muted rounded-lg" />}>
            <ProductFilters searchParams={searchParams} />
          </Suspense>
        </aside>

        {/* Conteúdo Principal */}
        <main className="flex-1">
          {/* Barra de Busca - FR-4 */}
          <div className="mb-6">
            <SearchBar
              defaultValue={searchParams.search}
              debounceMs={300} // Conforme spec: 300ms de debounce
            />
          </div>

          {/* Controles de Ordenação - FR-3 */}
          <div className="flex justify-between items-center mb-6">
            <p className="text-sm text-muted-foreground">
              Mostrando produtos
            </p>
            <SortSelect
              value={searchParams.sort}
              order={searchParams.order}
            />
          </div>

          {/* Grade de Produtos - FR-1 */}
          <Suspense fallback={<ProductGridSkeleton />}>
            <ProductGrid searchParams={searchParams} />
          </Suspense>
        </main>
      </div>
    </div>
  )
}

// Metadados para SEO
export async function generateMetadata({ searchParams }: ProductsPageProps) {
  const category = searchParams.category
  const search = searchParams.search

  let title = 'Produtos'
  let description = 'Navegue pelo nosso catálogo completo de produtos'

  if (search) {
    title = `Resultados da busca por "${search}"`
    description = `Encontre produtos correspondentes a "${search}"`
  } else if (category) {
    const categoryName = Array.isArray(category) ? category[0] : category
    title = `Produtos de ${categoryName}`
    description = `Navegue pela nossa coleção de ${categoryName}`
  }

  return {
    title,
    description,
    openGraph: {
      title,
      description,
    }
  }
}
// src/components/products/ProductFilters.tsx
'use client'

import { useRouter, useSearchParams, usePathname } from 'next/navigation'
import { useCallback, useTransition } from 'react'
import { Checkbox } from '@/components/ui/checkbox'
import { Slider } from '@/components/ui/slider'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { useQuery } from '@tanstack/react-query'

interface FilterData {
  categories: { id: string; name: string; slug: string; count: number }[]
  priceRange: { min: number; max: number }
}

export function ProductFilters({ searchParams }: { searchParams: any }) {
  const router = useRouter()
  const pathname = usePathname()
  const [isPending, startTransition] = useTransition()

  const { data: filterData, isLoading } = useQuery<FilterData>({
    queryKey: ['product-filters'],
    queryFn: async () => {
      const res = await fetch('/api/products?limit=1')
      const data = await res.json()
      return data.filters
    }
  })

  const updateFilters = useCallback((key: string, value: string | string[] | null) => {
    const params = new URLSearchParams(searchParams)

    if (value === null) {
      params.delete(key)
    } else if (Array.isArray(value)) {
      params.delete(key)
      value.forEach(v => params.append(key, v))
    } else {
      params.set(key, value)
    }

    // Redefinir para página 1 quando os filtros mudam
    params.delete('page')

    startTransition(() => {
      router.push(`${pathname}?${params.toString()}`)
    })
  }, [searchParams, pathname, router])

  const resetFilters = useCallback(() => {
    startTransition(() => {
      router.push(pathname)
    })
  }, [pathname, router])

  if (isLoading) {
    return <FiltersSkeleton />
  }

  const selectedCategories = Array.isArray(searchParams.category)
    ? searchParams.category
    : searchParams.category
      ? [searchParams.category]
      : []

  const currentMinPrice = Number(searchParams.minPrice) || filterData?.priceRange.min || 0
  const currentMaxPrice = Number(searchParams.maxPrice) || filterData?.priceRange.max || 10000

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <h2 className="font-semibold text-lg">Filtros</h2>
        <Button
          variant="ghost"
          size="sm"
          onClick={resetFilters}
          disabled={isPending}
          aria-label="Redefinir todos os filtros"
        >
          Redefinir
        </Button>
      </div>

      {/* Filtro de Categoria - FR-2: Seleção múltipla com lógica OR */}
      <div>
        <Label className="text-sm font-medium mb-3 block">Categoria</Label>
        <div className="space-y-2">
          {filterData?.categories.map((category) => (
            <div key={category.id} className="flex items-center space-x-2">
              <Checkbox
                id={`category-${category.slug}`}
                checked={selectedCategories.includes(category.slug)}
                onCheckedChange={(checked) => {
                  const newCategories = checked
                    ? [...selectedCategories, category.slug]
                    : selectedCategories.filter(c => c !== category.slug)
                  updateFilters('category', newCategories.length > 0 ? newCategories : null)
                }}
                aria-describedby={`category-${category.slug}-count`}
              />
              <label
                htmlFor={`category-${category.slug}`}
                className="text-sm leading-none cursor-pointer flex-1"
              >
                {category.name}
              </label>
              <span
                id={`category-${category.slug}-count`}
                className="text-xs text-muted-foreground"
              >
                ({category.count})
              </span>
            </div>
          ))}
        </div>
      </div>

      {/* Filtro de Faixa de Preço - FR-2: Slider duplo */}
      <div>
        <Label className="text-sm font-medium mb-3 block">
          Faixa de Preço
        </Label>
        <Slider
          min={filterData?.priceRange.min || 0}
          max={filterData?.priceRange.max || 10000}
          step={100}
          value={[currentMinPrice, currentMaxPrice]}
          onValueCommit={([min, max]) => {
            updateFilters('minPrice', String(min))
            updateFilters('maxPrice', String(max))
          }}
          className="mb-2"
          aria-label="Faixa de preço"
        />
        <div className="flex justify-between text-xs text-muted-foreground">
          <span>R$ ${(currentMinPrice / 100).toFixed(2)}</span>
          <span>R$ ${(currentMaxPrice / 100).toFixed(2)}</span>
        </div>
      </div>

      {/* Filtro de Disponibilidade - FR-2: Alternância */}
      <div className="flex items-center space-x-2">
        <Checkbox
          id="in-stock"
          checked={searchParams.inStock === 'true'}
          onCheckedChange={(checked) => {
            updateFilters('inStock', checked ? 'true' : null)
          }}
        />
        <label htmlFor="in-stock" className="text-sm cursor-pointer">
          Apenas em estoque
        </label>
      </div>

      {/* Indicador de carregamento */}
      {isPending && (
        <div
          className="text-sm text-muted-foreground animate-pulse"
          role="status"
          aria-label="Atualizando filtros"
        >
          Atualizando...
        </div>
      )}
    </div>
  )
}

function FiltersSkeleton() {
  return (
    <div className="space-y-6">
      <Skeleton className="h-6 w-20" />
      <div className="space-y-2">
        {[1, 2, 3, 4].map(i => (
          <Skeleton key={i} className="h-5 w-full" />
        ))}
      </div>
      <Skeleton className="h-8 w-full" />
      <Skeleton className="h-5 w-32" />
    </div>
  )
}

Passo 4: Verificar a Implementação em relação à Spec

Após a implementação, verifique se tudo corresponde à spec:

# No Claude Code
claude "Verifique a implementação do catálogo de produtos em relação a specs/features/products/catalog.spec.md:

1. Execute todos os testes e relate os resultados
2. Verifique cada requisito funcional (de FR-1 a FR-5)
3. Verifique se o contrato da API corresponde à spec
4. Verifique se o tratamento de erros corresponde à spec
5. Verifique se os casos de borda são tratados
6. Relate quaisquer desvios da spec"

O Claude Code executa a verificação:

Executando verificação em relação a catalog.spec.md...

✅ FR-1: Listagem de Produtos
   - Paginação: 12 produtos por padrão ✓
   - Campos do cartão do produto correspondem à spec ✓
   - Rolagem infinita configurável ✓

✅ FR-2: Filtragem
   - Seleção múltipla de categorias com lógica OR ✓
   - Slider duplo de faixa de preço ✓
   - Alternância de disponibilidade ✓
   - Filtros aplicados com lógica AND ✓

✅ FR-3: Ordenação
   - Preço (Menor para Maior padrão) ✓
   - Nome (A a Z padrão) ✓
   - Data de Adição (Mais Recente Primeiro padrão) ✓
   - Popularidade ✓
   - Avaliação ✓

✅ FR-4: Busca
   - Fuzzy matching (limite do Fuse.js 0.4) ✓
   - Mínimo de 2 caracteres ✓
   - 300ms de debounce ✓
   - Busca no nome e na descrição ✓

⚠️ FR-5: Detalhes do Produto
   - Galeria de imagens: FUNCIONALIDADE de zoom ausente
   - Produtos relacionados: Implementado ✓
   - Visualizados recentemente: Implementação de localStorage ausente

Verificação do Contrato da API:
✅ GET /api/products corresponde à spec
✅ GET /api/products/[slug] corresponde à spec

Tratamento de Erros:
✅ Página inválida: Retorna 400
✅ Filtros inválidos: Ignora inválidos, aplica válidos
✅ Produto não encontrado: Retorna 404
⚠️ Timeout de rede: Cache não implementado

Casos de Borda:
✅ Catálogo vazio tratado
✅ Todos os itens filtrados tratados
⚠️ Sugestões de busca: Não implementado
✅ Ajuste (clamp) de preço: Implementado

RESUMO:
- 23/27 requisitos atendidos
- 4 itens precisam de implementação:
  1. Funcionalidade de zoom na imagem
  2. localStorage para visualizados recentemente
  3. Cache stale-while-revalidate
  4. Sugestões de busca para quando não houver resultados

Padrão 2: Especificação de API com OpenAPI

Para projetos com uso intenso de API, use as especificações OpenAPI como sua fonte da verdade.

Criando a Spec OpenAPI

# No Claude Code
claude "Crie uma especificação OpenAPI 3.1 para nossa API de autenticação em specs/api/auth.openapi.yaml. Inclua:
- POST /api/auth/register
- POST /api/auth/login
- POST /api/auth/logout
- POST /api/auth/refresh
- POST /api/auth/forgot-password
- POST /api/auth/reset-password
- GET /api/auth/me

Siga estes requisitos:
- Tokens JWT com acesso de 15 min / atualização (refresh) de 7 dias
- Limite de taxa (rate limiting) no login (5 tentativas por 15 min)
- Verificação de e-mail obrigatória
- Requisitos de senha: 8+ caracteres, 1 letra maiúscula, 1 número"

O Claude Code gera:

# specs/api/auth.openapi.yaml
openapi: 3.1.0
info:
  title: API de Autenticação
  version: 1.0.0
  description: Endpoints de autenticação de usuário com tokens JWT

servers:
  - url: /api/auth
    description: Caminho base da API de Autenticação

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    User:
      type: object
      required: [id, email, name, emailVerified, createdAt]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 1
          maxLength: 100
        emailVerified:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    AuthTokens:
      type: object
      required: [accessToken, refreshToken, expiresAt]
      properties:
        accessToken:
          type: string
          description: Token de acesso JWT (expiração em 15 min)
        refreshToken:
          type: string
          description: Token de atualização (expiração em 7 dias)
        expiresAt:
          type: string
          format: date-time
          description: Tempo de expiração do token de acesso

    RegisterRequest:
      type: object
      required: [email, password, name]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
          pattern: ^(?=.*[A-Z])(?=.*\d).{8,}$
          description: "Mínimo de 8 caracteres, 1 maiúscula, 1 número"
        name:
          type: string
          minLength: 1
          maxLength: 100

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum:
            - invalid_credentials
            - email_not_verified
            - rate_limited
            - user_exists
            - invalid_token
            - token_expired
            - validation_error
        message:
          type: string
        retryAfter:
          type: integer
          description: Segundos até que o limite de taxa seja redefinido

paths:
  /register:
    post:
      operationId: register
      summary: Registrar novo usuário
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Usuário criado, e-mail de verificação enviado
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: "E-mail de verificação enviado"
                  user:
                    $ref: '#/components/schemas/User'
        '400':
          description: Erro de validação
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Usuário já existe
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /login:
    post:
      operationId: login
      summary: Autenticar usuário
      description: |
        Limite de taxa: 5 tentativas por 15 minutos por e-mail.
        Retorna 429 quando o limite é excedido.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Login bem-sucedido
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokens:
                    $ref: '#/components/schemas/AuthTokens'
                  user:
                    $ref: '#/components/schemas/User'
        '401':
          description: Credenciais inválidas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: E-mail não verificado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Limite de taxa excedido
          headers:
            Retry-After:
              schema:
                type: integer
              description: Segundos até que o limite seja redefinido
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /logout:
    post:
      operationId: logout
      summary: Invalidar token de atualização
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Logout realizado com sucesso
        '401':
          description: Token inválido ou expirado

  /refresh:
    post:
      operationId: refreshToken
      summary: Obter novo token de acesso usando o token de atualização
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken:
                  type: string
      responses:
        '200':
          description: Novos tokens emitidos
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokens'
        '401':
          description: Token de atualização inválido ou expirado

  /forgot-password:
    post:
      operationId: forgotPassword
      summary: Solicitar e-mail de redefinição de senha
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        '200':
          description: E-mail de redefinição enviado (sempre retorna 200 por segurança)

  /reset-password:
    post:
      operationId: resetPassword
      summary: Redefinir senha com token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token:
                  type: string
                password:
                  type: string
                  minLength: 8
                  pattern: ^(?=.*[A-Z])(?=.*\d).{8,}$
      responses:
        '200':
          description: Senha redefinida com sucesso
        '400':
          description: Token inválido ou expirado

  /me:
    get:
      operationId: getCurrentUser
      summary: Obter perfil do usuário atual
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Usuário atual
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '401':
          description: Não autenticado

Gerar Implementação a partir do OpenAPI

# No Claude Code
claude "Gere a implementação completa da API de autenticação a partir de specs/api/auth.openapi.yaml:

1. Crie todos os manipuladores de rota (route handlers) em src/app/api/auth/
2. Crie utilitários de autenticação em src/lib/auth/
3. Use jose para manipulação de JWT
4. Use upstash/redis para limite de taxa
5. Use resend para e-mails
6. Crie adições ao esquema Prisma para as tabelas de autenticação

Siga a spec OpenAPI exatamente."

Padrão 3: Especificação de Componentes para UI

Os componentes de UI se beneficiam de especificações precisas que definem comportamento, acessibilidade e estados visuais.

Criando uma Spec de Componente

# No Claude Code
claude "Crie uma especificação de componente para um componente DataTable em specs/components/data-table.spec.md. Requisitos:
- Paginação e ordenação no lado do servidor
- Alternância de visibilidade de colunas
- Seleção de linhas (única e múltipla)
- Edição em linha (inline editing)
- Exportação para CSV
- Navegação por teclado
- Acessibilidade total (WCAG 2.1 AA)"

O Claude Code gera:

# Especificação do Componente DataTable

## Visão Geral
Um componente de tabela de dados flexível e acessível com operações no lado do servidor.

## Interface de Props

```typescript
interface DataTableProps<TData> {
  // Dados
  data: TData[]
  columns: ColumnDef<TData>[]

  // Paginação (no lado do servidor)
  pageCount: number
  page: number
  pageSize: number
  onPageChange: (page: number) => void
  onPageSizeChange: (size: number) => void
  pageSizeOptions?: number[] // Padrão: [10, 25, 50, 100]

  // Ordenação (no lado do servidor)
  sortColumn?: string
  sortDirection?: 'asc' | 'desc'
  onSort: (column: string, direction: 'asc' | 'desc') => void

  // Seleção
  selectionMode?: 'none' | 'single' | 'multi'
  selectedRows?: string[] // IDs das linhas
  onSelectionChange?: (selectedIds: string[]) => void
  getRowId: (row: TData) => string

  // Visibilidade de colunas
  hiddenColumns?: string[]
  onColumnVisibilityChange?: (columns: string[]) => void

  // Edição em linha (inline editing)
  editableColumns?: string[]
  onCellEdit?: (rowId: string, column: string, value: unknown) => Promise<void>

  // Exportação
  exportFilename?: string
  onExport?: () => void // Manipulador de exportação personalizado

  // Estados de carregamento
  isLoading?: boolean

  // Acessibilidade
  caption: string // Obrigatório para leitores de tela
  ariaLabel?: string
}
```

## Estados Visuais

### Estados de Linha
| Estado | Segundo Plano (Background) | Borda | Descrição |
|--------|----------------------------|-------|-----------|
| Padrão | transparente | nenhuma | Linha normal |
| Hover | bg-muted/50 | nenhuma | Mouse sobre a linha |
| Selecionado | bg-primary/10 | left-4 primary | Linha selecionada |
| Editando | bg-warning/10 | left-4 warning | Célula sendo editada |
| Desativado | opacity-50 | nenhuma | Linha não interativa |

### Estados de Célula
| Estado | Aparência | Gatilho |
|--------|-----------|---------|
| Padrão | Texto normal | — |
| Ordenando | Negrito + ícone | Coluna de ordenação ativa |
| Editável | Sublinhado tracejado | Coluna editável |
| Editando | Campo de entrada | Clique duplo ou Enter |
| Salvando | Spinner | Após o envio da edição |
| Erro | Borda vermelha | Falha ao salvar |

## Navegação por Teclado

### Gerenciamento de Foco
- A tabela é focável com Tab
- As setas do teclado navegam entre as células
- O foco fica retido dentro da tabela ao navegar

### Atalhos de Teclado
| Tecla | Ação |
|-------|------|
| Tab | Mover para o próximo elemento focável |
| Shift+Tab | Mover para o elemento focável anterior |
| Seta Cima/Baixo | Mover entre as linhas |
| Seta Esquerda/Direita | Mover entre as células |
| Home | Primeira célula da linha |
| End | Última célula da linha |
| Ctrl+Home | Primeira célula da tabela |
| Ctrl+End | Última célula da tabela |
| Espaço | Alternar seleção da linha (quando na linha) |
| Enter | Editar célula (se editável) / Confirmar edição |
| Escape | Cancelar edição / Limpar seleção |
| Ctrl+A | Selecionar todas as linhas (modo seleção múltipla) |
| Ctrl+Shift+E | Exportar para CSV |

## Requisitos de Acessibilidade

### Atributos ARIA
```tsx
<table
  role="grid"
  aria-label={ariaLabel}
  aria-describedby="table-caption"
  aria-rowcount={totalRows}
  aria-colcount={columns.length}
  aria-busy={isLoading}
>
  <caption id="table-caption" className="sr-only">
    {caption}
  </caption>
  <thead>
    <tr role="row">
      <th
        role="columnheader"
        aria-sort={sortDirection}
        scope="col"
      >
        {/* Cabeçalho da coluna */}
      </th>
    </tr>
  </thead>
  <tbody>
    <tr
      role="row"
      aria-rowindex={rowIndex}
      aria-selected={isSelected}
    >
      <td
        role="gridcell"
        aria-colindex={colIndex}
        tabIndex={isFocused ? 0 : -1}
      >
        {/* Conteúdo da célula */}
      </td>
    </tr>
  </tbody>
</table>
```

### Anúncios de Leitor de Tela
- Mudança de ordenação: "Ordenado por {column} {direction}"
- Mudança de seleção: "{count} linhas selecionadas"
- Mudança de página: "Página {n} de {total}"
- Início de edição: "Editando {column}"
- Edição concluída: "{column} atualizado" / "Edição cancelada"
- Exportação: "Exportando {count} linhas para CSV"

### Indicadores de Foco
- Anel de foco visível claro (2px sólido primary)
- Suporte ao modo de alto contraste
- Nunca remover o contorno (outline) de foco

## Comportamento Responsivo

### Pontos de Quebra (Breakpoints)
| Ponto de Quebra | Comportamento |
|-----------------|---------------|
| < 640px (sm) | Rolagem horizontal, primeira coluna fixa (sticky) |
| 640-1024px (md) | Mostrar colunas prioritárias, ocultar outras |
| > 1024px (lg) | Mostrar todas as colunas |

### Prioridade de Coluna
As colunas têm prioridade (1-10). Em telas menores, as colunas de prioridade mais baixa são ocultadas primeiro.

```typescript
interface ColumnDef<T> {
  id: string
  header: string
  priority?: number // 1 = sempre visível, 10 = ocultar primeiro
  minWidth?: number
  // ...
}
```

## Tratamento de Erros

| Erro | Resposta da UI |
|------|----------------|
| Falha na busca de dados | Mostrar estado de erro com botão de repetir |
| Falha ao salvar edição | Mostrar toast de erro, reverter valor da célula |
| Falha na exportação | Mostrar toast de erro com detalhes |
| Página inválida | Redefinir para página 1 |

## Requisitos de Desempenho

- Renderização inicial: < 100ms para 100 linhas
- Resposta de ordenação/filtro: < 50ms percebidos (UI otimista)
- Rolagem: 60fps mantidos
- Memória: < 50MB para 10.000 linhas
- Virtualização: Obrigatória para > 500 linhas

## Casos de Teste

### Renderização
- [ ] Renderiza o número correto de linhas
- [ ] Renderiza todas as colunas visíveis
- [ ] Aplica formatação de célula correta
- [ ] Mostra estado de carregamento
- [ ] Mostra estado vazio

### Paginação
- [ ] Exibe informações de página corretas
- [ ] Navega para a página seguinte/anterior
- [ ] Altera o tamanho da página
- [ ] Desativa o botão anterior na primeira página
- [ ] Desativa o próximo na última página

### Ordenação
- [ ] Ordena de forma ascendente no primeiro clique
- [ ] Ordena de forma descendente no segundo clique
- [ ] Limpa a ordenação no terceiro clique
- [ ] Mostra indicador de ordenação
- [ ] Mantém a ordenação entre as páginas

### Seleção
- [ ] Modo de seleção única funciona
- [ ] Modo de seleção múltipla funciona
- [ ] "Selecionar todos" seleciona a página atual
- [ ] Seleção de faixa com Shift+clique
- [ ] Seleção persiste entre as páginas

### Edição
- [ ] Clique duplo entra no modo de edição
- [ ] Enter confirma a edição
- [ ] Escape cancela a edição
- [ ] Tab move para a próxima célula editável
- [ ] Mostra indicador de salvamento
- [ ] Reverte em caso de erro

### Teclado
- [ ] Tab foca na tabela
- [ ] Teclas de seta navegam pelas células
- [ ] Home/End funcionam corretamente
- [ ] Espaço alterna a seleção
- [ ] Todos os atalhos funcionam

### Acessibilidade
- [ ] Todos os atributos ARIA presentes
- [ ] Leitor de tela anuncia as mudanças
- [ ] Gerenciamento de foco correto
- [ ] Modo de alto contraste funciona
- [ ] Movimento reduzido respeitado

Gerar Componente a partir da Spec

# No Claude Code
claude "Implemente o componente DataTable a partir de specs/components/data-table.spec.md:

1. Crie src/components/ui/data-table/DataTable.tsx
2. Crie os componentes de suporte (header, row, cell, pagination)
3. Crie hooks para navegação por teclado e seleção
4. Use @tanstack/react-table para a funcionalidade principal
5. Use Tailwind CSS para estilização
6. Inclua todos os recursos de acessibilidade da spec
7. Crie testes cobrindo todos os casos de teste da spec"

Padrão 4: Especificações Orientadas a Comportamento (BDD)

Para lógica de negócios complexa, use especificações no estilo Gherkin que definem o comportamento por meio de cenários.

Criando Specs BDD

# No Claude Code
claude "Crie uma especificação BDD para a funcionalidade de carrinho de compras em specs/features/cart/cart.feature. Cubra:
- Adicionar itens ao carrinho
- Atualizar quantidades
- Remover itens
- Aplicar códigos de desconto
- Persistência do carrinho
- Validação de estoque"

O Claude Code gera:

# specs/features/cart/cart.feature
Funcionalidade: Carrinho de Compras
  Como um cliente
  Eu quero gerenciar itens no meu carrinho de compras
  Para que eu possa comprar produtos

  Contexto:
    Dado que o catálogo de produtos contém:
      | sku     | nome           | preço  | estoque |
      | SHOE-01 | Tênis de Corrida | 129.99 | 10    |
      | SHIRT-L | Camisa Azul (G) | 49.99  | 5     |
      | HAT-BLK | Boné Preto      | 24.99  | 0     |

  # Adicionando Itens

  Cenário: Adicionar um único item ao carrinho vazio
    Dado que eu tenho um carrinho vazio
    Quando eu adiciono "SHOE-01" ao meu carrinho
    Então meu carrinho deve conter 1 item
    E o total do carrinho deve ser $129.99

  Cenário: Adicionar múltiplas quantidades do mesmo item
    Dado que eu tenho um carrinho vazio
    Quando eu adiciono "SHOE-01" com quantidade 2 ao meu carrinho
    Então meu carrinho deve conter 2 itens
    E o total do carrinho deve ser $259.98

  Cenário: Adicionar item que está fora de estoque
    Dado que eu tenho um carrinho vazio
    Quando eu tento adicionar "HAT-BLK" ao meu carrinho
    Então eu devo ver um erro "Este item está fora de estoque"
    E meu carrinho deve estar vazio

  Cenário: Adicionar mais do que o estoque disponível
    Dado que eu tenho um carrinho vazio
    Quando eu tento adicionar "SHIRT-L" com quantidade 10 ao meu carrinho
    Então eu devo ver um erro "Apenas 5 itens disponíveis"
    E meu carrinho deve conter 5 itens de "SHIRT-L"

  # Atualizando Quantidades

  Cenário: Aumentar a quantidade de um item
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    Quando eu atualizo a quantidade de "SHOE-01" para 3
    Então meu carrinho deve conter 3 itens de "SHOE-01"
    E o total do carrinho deve ser $389.97

  Cenário: Diminuir a quantidade de um item
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 3          |
    Quando eu atualizo a quantidade de "SHOE-01" para 1
    Então meu carrinho deve conter 1 item de "SHOE-01"

  Cenário: Definir a quantidade como zero remove o item
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 2          |
    Quando eu atualizo a quantidade de "SHOE-01" para 0
    Então meu carrinho não deve conter "SHOE-01"

  # Removendo Itens

  Cenário: Remover um único item do carrinho
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
      | SHIRT-L | 2          |
    Quando eu removo "SHOE-01" do meu carrinho
    Então meu carrinho não deve conter "SHOE-01"
    E meu carrinho deve conter 2 itens de "SHIRT-L"

  Cenário: Remover o último item esvazia o carrinho
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    Quando eu removo "SHOE-01" do meu carrinho
    Então meu carrinho deve estar vazio

  # Códigos de Desconto

  Cenário: Aplicar desconto percentual válido
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    E que existe um código de desconto "SAVE20" de 20%
    Quando eu aplico o código de desconto "SAVE20"
    Então o desconto deve ser $26.00
    E o total do carrinho deve ser $103.99

  Cenário: Aplicar desconto de valor fixo válido
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    E que existe um código de desconto "FLAT50" de $50 para pedidos acima de $100
    Quando eu aplico o código de desconto "FLAT50"
    Então o desconto deve ser $50.00
    E o total do carrinho deve ser $79.99

  Cenário: Aplicar código de desconto inválido
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    Quando eu aplico o código de desconto "INVALID"
    Então eu devo ver um erro "Código de desconto inválido"
    E nenhum desconto deve ser aplicado

  Cenário: Aplicar código de desconto expirado
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    E que existe um código de desconto expirado "OLD10"
    Quando eu aplico o código de desconto "OLD10"
    Então eu devo ver um erro "Este código de desconto expirou"

  Cenário: Aplicar desconto com exigência de pedido mínimo
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | HAT-BLK | 1          |
    E que existe um código de desconto "BIG100" de $20 para pedidos acima de $100
    Quando eu aplico o código de desconto "BIG100"
    Então eu devo ver um erro "Pedido mínimo de $100 necessário"

  Cenário: Remover código de desconto
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    E que o código de desconto "SAVE20" está aplicado
    Quando eu removo o código de desconto
    Então nenhum desconto deve ser aplicado
    E o total do carrinho deve ser $129.99

  # Persistência do Carrinho

  Cenário: O carrinho persiste após a atualização da página
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 2          |
    Quando eu atualizo a página
    Então meu carrinho deve conter 2 itens de "SHOE-01"

  Cenário: O carrinho sincroniza entre abas
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    Quando eu abro uma nova aba no navegador
    E eu adiciono "SHIRT-L" ao meu carrinho na nova aba
    E eu volto para a aba original
    Então meu carrinho deve conter "SHOE-01" e "SHIRT-L"

  Cenário: O carrinho de visitante é mesclado ao fazer login
    Dado que eu tenho um carrinho de visitante contendo:
      | sku     | quantidade |
      | SHOE-01 | 1          |
    E que eu tenho um carrinho salvo na minha conta contendo:
      | sku     | quantidade |
      | SHIRT-L | 2          |
    Quando eu faço login na minha conta
    Então meu carrinho deve conter:
      | sku     | quantidade |
      | SHOE-01 | 1          |
      | SHIRT-L | 2          |

  # Validação de Estoque

  Cenário: O estoque diminui enquanto o item está no carrinho
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 5          |
    E que o estoque de "SHOE-01" diminui para 3
    Quando eu visualizo meu carrinho
    Então eu devo ver um aviso "Apenas 3 disponíveis, quantidade ajustada"
    E meu carrinho deve conter 3 itens de "SHOE-01"

  Cenário: O item fica fora de estoque enquanto está no carrinho
    Dado que meu carrinho contém:
      | sku     | quantidade |
      | SHOE-01 | 2          |
    E que "SHOE-01" fica fora de estoque
    Quando eu visualizo meu carrinho
    Então eu devo ver um erro "Este item não está mais disponível"
    E o item deve ser marcado como não disponível

Gerar Implementação a partir da Spec BDD

# No Claude Code
claude "Implemente a funcionalidade de carrinho de compras a partir de specs/features/cart/cart.feature:

1. Crie o gerenciamento de estado do carrinho com Zustand em src/stores/cartStore.ts
2. Crie rotas de API do carrinho em src/app/api/cart/
3. Crie componentes de UI do carrinho em src/components/cart/
4. Crie testes Playwright a partir dos cenários Gherkin
5. Use localStorage para persistência do carrinho de visitante
6. Sincronize com o servidor para usuários logados"

Padrão 5: Especificações de Migração

Ao refatorar ou migrar sistemas, as especificações definem o que deve ser preservado.

Criando uma Spec de Migração

# No Claude Code
claude "Crie uma especificação de migração para mudar do Pages Router para o App Router em specs/migrations/pages-to-app-router.spec.md. Documente:
- Todas as páginas atuais e suas novas localizações
- Plano de migração de rotas de API
- Mudanças na busca de dados (getServerSideProps → Server Components)
- Mudanças de Middleware
- Requisitos de teste"

O Claude Code gera:

# Especificação de Migração: Pages Router para App Router

## Visão Geral
Migrar a aplicação Next.js do Pages Router para o App Router mantendo todas as funcionalidades.

## Análise do Estado Atual

### Inventário de Páginas
| Caminho Atual | Novo Caminho | Busca de Dados (Data Fetching) | Notas |
|---------------|--------------|--------------------------------|-------|
| pages/index.tsx | app/page.tsx | getServerSideProps → RSC | Página inicial |
| pages/products/index.tsx | app/products/page.tsx | getServerSideProps → RSC | Listagem de produtos |
| pages/products/[slug].tsx | app/products/[slug]/page.tsx | getStaticPaths + getStaticProps → generateStaticParams | Detalhes do produto |
| pages/cart.tsx | app/cart/page.tsx | Apenas no lado do cliente | Carrinho de compras |
| pages/checkout/index.tsx | app/checkout/page.tsx | getServerSideProps → RSC | Checkout |
| pages/checkout/success.tsx | app/checkout/success/page.tsx | getServerSideProps → RSC | Confirmação de pedido |
| pages/account/index.tsx | app/account/page.tsx | Rota protegida | Conta do usuário |
| pages/account/orders.tsx | app/account/orders/page.tsx | Rota protegida | Histórico de pedidos |
| pages/auth/login.tsx | app/auth/login/page.tsx | Lado do cliente | Formulário de login |
| pages/auth/register.tsx | app/auth/register/page.tsx | Lado do cliente | Registro |
| pages/404.tsx | app/not-found.tsx | Estático | Página 404 |
| pages/500.tsx | app/error.tsx | Estático | Página de erro |

### Migração de Rotas de API
| Caminho Atual | Novo Caminho | Mudanças |
|---------------|--------------|----------|
| pages/api/products/*.ts | app/api/products/route.ts | Combinar em route handlers |
| pages/api/cart/*.ts | app/api/cart/route.ts | Usar Route Handlers |
| pages/api/auth/[...nextauth].ts | app/api/auth/[...nextauth]/route.ts | Configuração do App Router para NextAuth |
| pages/api/checkout/*.ts | app/api/checkout/route.ts | Alternativa com Server Actions |

## Regras de Migração

### Transformação da Busca de Dados

**Antes (getServerSideProps):**
```typescript
export async function getServerSideProps({ params }) {
  const product = await fetchProduct(params.slug)
  return { props: { product } }
}

export default function ProductPage({ product }) {
  return <ProductDetail product={product} />
}
```

**Depois (Server Component):**
```typescript
async function ProductPage({ params }) {
  const product = await fetchProduct(params.slug)
  return <ProductDetail product={product} />
}

export default ProductPage
```

### Componentes de Cliente
Adicione a diretiva 'use client' para:
- Componentes que usam useState, useEffect
- Componentes que usam APIs do navegador
- Componentes com manipuladores de eventos
- Componentes que usam bibliotecas de lado do cliente

### Layouts
Crie arquivos layout.tsx para a UI compartilhada:
```
app/
├── layout.tsx           # Layout raiz (HTML, body, providers)
├── (shop)/
│   ├── layout.tsx       # Layout da loja (cabeçalho, rodapé)
│   ├── products/
│   └── cart/
├── (account)/
│   ├── layout.tsx       # Layout da conta (barra lateral)
│   └── account/
└── (auth)/
    ├── layout.tsx       # Layout de autenticação (cartão centralizado)
    └── auth/
```

### Metadados
Substitua next/head pela API de Metadados:

**Antes:**
```typescript
import Head from 'next/head'

export default function Page() {
  return (
    <>
      <Head>
        <title>Nome do Produto</title>
        <meta name="description" content="..." />
      </Head>
      {/* conteúdo */}
    </>
  )
}
```

**Depois:**
```typescript
export async function generateMetadata({ params }) {
  const product = await fetchProduct(params.slug)
  return {
    title: product.name,
    description: product.description,
  }
}
```

## Checklist de Verificação

### Requisitos Funcionais
- [ ] Todas as páginas são renderizadas corretamente
- [ ] Todos os endpoints da API retornam as mesmas respostas
- [ ] O fluxo de autenticação funciona
- [ ] Funcionalidade do carrinho preservada
- [ ] O processo de checkout funciona
- [ ] O histórico de pedidos é exibido corretamente

### Requisitos de Desempenho
- [ ] LCP igual ou melhor do que antes
- [ ] FCP igual ou melhor do que antes
- [ ] TTI igual ou melhor do que antes
- [ ] Tamanho do bundle igual ou menor

### Requisitos de SEO
- [ ] Todas as meta tags preservadas
- [ ] URLs canônicas corretas
- [ ] Tags Open Graph presentes
- [ ] Sitemap gerado corretamente
- [ ] robots.txt sem alterações

### Requisitos de Acessibilidade
- [ ] Todos os atributos ARIA preservados
- [ ] A navegação por teclado funciona
- [ ] Compatibilidade com leitor de tela
- [ ] Gerenciamento de foco correto

## Estratégia de Teste

### Antes da Migração (Linha de Base - Baseline)
1. Execute a suíte completa de testes E2E
2. Capture as pontuações do Lighthouse para todas as páginas
3. Documente todos os esquemas de resposta da API
4. Tire capturas de tela de todas as páginas nos pontos de quebra

### Durante a Migração
1. Migre um grupo de rotas por vez
2. Execute testes E2E após cada grupo
3. Compare as respostas da API com a linha de base
4. Testes de regressão visual

### Após a Migração
1. A suíte completa de testes E2E deve passar
2. As pontuações do Lighthouse devem corresponder à linha de base
3. Os esquemas de resposta da API devem ser iguais
4. A regressão visual deve passar

## Plano de Rollback
- Mantenha o diretório pages/ até que a migração seja concluída
- Use feature flags para alternar entre os roteadores
- Esquema do banco de dados inalterado (rollback não é necessário)
- Mantenha uma branch git com o estado pré-migração

Referência de Comandos do Claude Code

Aqui está uma referência abrangente de comandos do Claude Code para o desenvolvimento orientado a especificações.

Criando Especificações

# Criar especificação de funcionalidade
claude "Crie uma especificação de funcionalidade para [funcionalidade] em specs/features/[dominio]/[funcionalidade].spec.md"

# Criar especificação de API (OpenAPI)
claude "Crie uma especificação OpenAPI 3.1 para [API] em specs/api/[api].openapi.yaml"

# Criar especificação de componente
claude "Crie uma especificação de componente para [Componente] em specs/components/[componente].spec.md"

# Criar especificação BDD
claude "Crie uma especificação BDD para [funcionalidade] em specs/features/[dominio]/[funcionalidade].feature"

# Criar especificação de migração
claude "Crie uma especificação de migração para [migração] em specs/migrations/[migração].spec.md"

Validando Especificações

# Verificar a completude de uma especificação
claude "Revise specs/[caminho] e identifique quaisquer requisitos ausentes, casos de borda ou ambiguidades"

# Validar em relação ao código existente
claude "Compare specs/[caminho] com src/[caminho] e relate quaisquer desvios"

# Verificar a consistência da especificação
claude "Verifique se specs/features/[dominio]/*.spec.md são consistentes com specs/api/[api].openapi.yaml"

Gerando a partir de Especificações

# Gerar testes a partir da spec
claude "Gere testes a partir de specs/[caminho] em tests/[caminho-correspondente]"

# Gerar implementação a partir da spec
claude "Implemente [funcionalidade] com base em specs/[caminho] em src/[caminho]"

# Gerar tipos a partir da spec OpenAPI
claude "Gere tipos TypeScript a partir de specs/api/[api].openapi.yaml em src/types/[api].ts"

# Gerar documentação a partir da spec
claude "Gere documentação de API a partir de specs/api/[api].openapi.yaml em docs/api/[api].md"

Comandos de Verificação

# Verificar se a implementação corresponde à spec
claude "Verifique se a implementação em src/[caminho] corresponde a specs/[caminho] e relate desvios"

# Executar testes baseados na spec
claude "Execute todos os testes derivados de specs/[caminho] e relate os resultados"

# Verificar cobertura em relação à spec
claude "Verifique quais requisitos de specs/[caminho] têm cobertura de teste"

Atualizando Especificações

# Atualizar spec com base em mudanças na implementação
claude "Atualize specs/[caminho] para refletir as mudanças feitas em src/[caminho]"

# Adicionar casos de borda à spec
claude "Adicione casos de borda em specs/[caminho] com base em bugs encontrados em produção"

# Versionar mudanças na spec
claude "Crie uma entrada de changelog para as mudanças em specs/[caminho]"

Melhores Práticas para o Desenvolvimento Orientado a Especificações

1. Spec Antes do Código, Sempre

A tentação de "apenas começar a codar" é forte. Resista a ela. Cada hora gasta em especificações economiza 10 horas de retrabalho.

Ruim:

Usuário: Adicione um formulário de inscrição na newsletter
Desenvolvedor: *começa a codar imediatamente*

Bom:

Usuário: Adicione um formulário de inscrição na newsletter
Desenvolvedor: *cria a spec primeiro*
- Quais campos? (apenas e-mail ou nome + e-mail?)
- Confirmação dupla (double opt-in) necessária?
- O que acontece em caso de inscrição duplicada?
- Limite de taxa (rate limiting)?
- Mensagens de sucesso/erro?
- Eventos de analytics?

2. Especificações São Documentos Vivos

As especificações evoluem. Quando os requisitos mudam:

  1. Atualize a spec primeiro
  2. Atualize os testes para corresponderem à nova spec
  3. Atualize a implementação
  4. Verifique se a implementação corresponde à spec

Nunca atualize o código sem atualizar a spec.

3. Nível de Detalhe Adequado

Vago demais: "Usuários podem pesquisar produtos"

Detalhado demais: "O campo de busca tem fonte de 16px, preenchimento (padding) de 12px e usa a família de fontes Inter"

Na medida certa: "A busca retorna correspondências difusas no nome e na descrição do produto, mínimo de 2 caracteres, 300ms de debounce"

4. Separação de Preocupações nas Specs

Crie specs diferentes para:

  • Specs de funcionalidades: Lógica de negócios e fluxos de usuário
  • Specs de API: Contratos e formatos de dados
  • Specs de componentes: Comportamento de UI e acessibilidade
  • Specs de migração: O que deve ser preservado

5. Inclua o que NÃO Está Incluído

Declare explicitamente o que está fora de escopo:

## Fora de Escopo
- Atualizações de estoque em tempo real (fase futura)
- Integração com lista de desejos (funcionalidade separada)
- Preços de atacado B2B (não é MVP)

Isso evita o aumento do escopo (scope creep) e mal-entendidos.

6. Casos de Teste nas Specs

Inclua casos de teste nas especificações. Eles:

  • Esclarecem requisitos por meio de exemplos
  • Tornam-se a base para testes automatizados
  • Servem como critérios de aceitação

7. Desempenho nas Specs

Sempre inclua requisitos de desempenho:

## Requisitos de Desempenho
- Resposta de busca: < 200ms p95
- Carregamento da página: < 2s em 3G
- Sem mudança de layout (layout shift) após a renderização inicial

Sem isso, "funciona" pode significar "leva 10 segundos para carregar".

8. Acessibilidade nas Specs

Inclua requisitos de acessibilidade:

## Requisitos de Acessibilidade
- Navegação por teclado para todos os elementos interativos
- Anúncios de leitor de tela para conteúdo dinâmico
- Proporção de contraste de cores mínima de 4.5:1
- Indicadores de foco visíveis

A acessibilidade adicionada posteriormente é cara. Especifique-a desde o início.


Armadilhas Comuns e Como Evitá-las

Armadilha 1: A Spec Fica Obsoleta

Problema: A spec foi escrita, a implementação divergiu e a spec nunca foi atualizada.

Solução:

  • Considere as atualizações da spec como parte do PR
  • Use verificações de CI que validem o alinhamento da spec com o código
  • Realize sessões regulares de revisão de specs

Armadilha 2: Super-especificação

Problema: A spec tem 50 páginas e leva mais tempo para escrever do que para implementar.

Solução:

  • Foque no comportamento, não nos detalhes da implementação
  • Use exemplos em vez de texto corrido
  • Divida specs grandes em specs menores e focadas

Armadilha 3: Sub-especificação

Problema: A spec é vaga, e a implementação ainda exige suposições.

Solução:

  • Inclua exemplos concretos
  • Defina os casos de borda explicitamente
  • Peça para outra pessoa revisar a spec antes da implementação

Armadilha 4: Ignorar a Spec Durante a Implementação

Problema: O desenvolvedor lê a spec uma vez e depois a ignora.

Solução:

  • Gere testes a partir da spec antes de codar
  • Use a spec como um checklist durante a implementação
  • Verifique em relação à spec antes do PR

Armadilha 5: Spec Apenas como Documentação

Problema: A spec existe, mas não é usada no fluxo de trabalho.

Solução:

  • Integre a spec ao fluxo de trabalho do Claude Code
  • Gere código a partir da spec, não do zero
  • Torne a verificação de specs parte do CI

Medindo o Sucesso com o Desenvolvimento Orientado a Especificações

Métricas para Acompanhar

1. Taxa de Acerto na Primeira Tentativa Percentual de implementações que passam em todos os testes na primeira tentativa.

  • Antes do SDD: ~30%
  • Depois do SDD: ~75%+

2. Impacto da Mudança de Requisitos Tempo para implementar mudanças de requisitos.

  • Antes do SDD: Alto (as mudanças cascateiam pelo código)
  • Depois do SDD: Baixo (mudança na spec → mudança no teste → mudança no código)

3. Densidade de Bugs Bugs por 1.000 linhas de código.

  • Antes do SDD: ~15-25
  • Depois do SDD: ~2-5

4. Tempo de Revisão de Código Tempo gasto na revisão do código.

  • Antes do SDD: Longo (revisando requisitos E implementação)
  • Depois do SDD: Curto (implementação em relação a uma spec conhecida)

5. Tempo de Integração (Onboarding) Tempo para um novo desenvolvedor entender uma funcionalidade.

  • Antes do SDD: Dias (lendo código, fazendo perguntas)
  • Depois do SDD: Horas (ler a spec, entender a intenção)

Conclusão: A Vantagem de Ser Orientado a Especificações

Desenvolvimento orientado a especificações não se trata de escrever mais documentação. Trata-se de pensar antes de codar.

Com assistentes de codificação de IA como o Claude Code, as especificações tornam-se ainda mais poderosas:

  1. Specs claras = melhor saída da IA: A IA gera código correto na primeira tentativa
  2. Specs como testes: Gere testes abrangentes a partir de especificações
  3. Loop de verificação: Verifique a implementação em relação à spec automaticamente
  4. Captura de conhecimento: As specs documentam decisões para referência futura

Os desenvolvedores que adotarem fluxos de trabalho orientados a especificações:

  • Construirão a coisa certa, logo na primeira vez
  • Gastarão menos tempo depurando e retrabalhando
  • Criarão bases de código mais fáceis de manter
  • Integrarão novos membros da equipe mais rapidamente
  • Entregarão com maior confiança

O custo? Algumas horas adiantadas escrevendo especificações.

O benefício? Centenas de horas economizadas em retrabalho, depuração e falhas de comunicação.

Comece hoje. Escolha uma funcionalidade. Escreva a spec primeiro. Depois, deixe o Claude Code implementá-la.

Você nunca mais voltará ao desenvolvimento focado primeiro no código.


Referência Rápida: Checklist de Desenvolvimento Orientado a Especificações

Antes de Começar Qualquer Funcionalidade

  • Criar especificação da funcionalidade
  • Revisar a spec com as partes interessadas
  • Identificar casos de borda
  • Definir tratamento de erros
  • Especificar requisitos de desempenho
  • Incluir requisitos de acessibilidade
  • Obter aprovação da spec

Durante a Implementação

  • Gerar testes a partir da spec
  • Implementar em relação à spec (não de forma ad-hoc)
  • Marcar cada requisito conforme for implementado
  • Verificar se os casos de borda foram tratados
  • Executar testes continuamente

Antes de Entregar

  • Todos os testes passam
  • A implementação corresponde à spec
  • A spec está atualizada
  • Requisitos de desempenho atendidos
  • Requisitos de acessibilidade atendidos
  • Documentação atualizada

Artigos Relacionados


Comece com uma spec. Deixe o Claude Code fazer o resto.

Anderson Lima

AI Engineer

Building the internet.

Related Articles

Continue exploring similar topics