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.

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:
- O quê a funcionalidade faz (requisitos funcionais)
- Como ela deve se comportar (contratos de comportamento)
- Quando ela falha (especificações de tratamento de erros)
- 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:
- Atualize a spec primeiro
- Atualize os testes para corresponderem à nova spec
- Atualize a implementação
- 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:
- Specs claras = melhor saída da IA: A IA gera código correto na primeira tentativa
- Specs como testes: Gere testes abrangentes a partir de especificações
- Loop de verificação: Verifique a implementação em relação à spec automaticamente
- 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
- LLM Pair Programming Patterns - Padrões fundamentais para o desenvolvimento assistido por IA (em inglês)
- MCP Sem Segredos - O Manual Definitivo para Turbinar Seu Workflow com IA
- Claude Code Skills Mastery - Domine as habilidades do Claude Code
Comece com uma spec. Deixe o Claude Code fazer o resto.
Anderson Lima
AI Engineer
Building the internet.
Related Articles
Continue exploring similar topics

React2Shell, Next.js e React Server Components: a vulnerabilidade que inaugura a era dos exploits acelerados por IA
A CVE-2025-55182, conhecida como React2Shell, revelou uma falha crítica em React Server Components e Next.js que permite RCE não autenticado e já está sendo explorada em produção — muitas vezes com PoCs gerados por IA. Neste artigo, mostro o que aconteceu, como saber se seu app está vulnerável, o que fazer agora e por que incidentes assim vão ficar cada vez mais comuns na era da inteligência artificial.

Como criei um sistema de notificações integrado ao Claude Code no macOS
Como criei um sistema avançado de notificações no macOS totalmente integrado ao Claude Code, usando shell script, AppleScript e automação inteligente para aumentar produtividade, sincronizar alertas com iPhone/iPad e melhorar fluxos de desenvolvimento.

Claude Code vs Codex vs Gemini: quem venceu a batalha dos agentes de IA para desenvolvedores
Nos últimos meses, os agentes de Inteligência Artificial deixaram de ser apenas copilotos de código e passaram a agir como verdadeiros engenheiros virtuais. Ferramentas como Claude Code, Codex CLI e Gemini CLI estão mudando completamente a forma como desenvolvedores escrevem, testam e otimizam código.