Como estruturar código, contratos e contexto para que ferramentas de IA gerem UI e APIs sem adivinhar o que você quis dizer
Definidos os papéis do time AI-native, como codar de fato?
A resposta curta é: pare de escrever código que só humanos leem. Escreva código que também é lido por uma ferramenta de IA que vai estender, refatorar e testar esse código. Isso não significa entregar decisões ao modelo — significa tornar o trabalho legível o suficiente para que ele execute o que você descreveu, em vez de adivinhar.
A diferença entre um time que se diz AI-native e um time que de fato codifica AI-native aparece em três lugares: na forma como o contexto é entregue ao modelo, na existência de contratos verificáveis (specs, schemas) e na clareza do domain logic. Sem esses pilares, a IA gera trabalho extra e produz código que parece certo mas quebra em produção.
Este artigo cobre o que muda no frontend (componentes gerados, design tokens como contexto, spec-driven UI) e no backend (APIs como contrato, tool definitions via Model Context Protocol, semantic layer). O foco é em padrões concretos: TypeScript que compila, Mermaid que renderiza e anti-patterns que você consegue evitar ainda hoje.
Durante anos escrevemos código para dois leitores: humanos e compiladores. Agora há um terceiro leitor que lê o código sob demanda e produz mais código com base nele. Esse leitor é determinístico na forma como consome contexto, mas estatístico na forma como infere intenção. Quando o contexto é vago, ele chuta. Quando é explícito, ele replica.
O princípio central é simples:
Toda informação necessária para gerar ou alterar uma parte do sistema deve estar explícita em algum artefato legível por máquina.
Isso reorganiza prioridades antigas:
| Princípio tradicional | Prioridade AI-native | Por quê |
|---|---|---|
| Código limpo para humanos | Código limpo para humanos E legível para LLM | O LLM lê mais frequentemente que o revisor |
| Documentação externa | Schema + JSDoc vivendo no código | Documentação externa não chega ao prompt |
| API REST com exemplo no README | API com schema OpenAPI/zod | Schema gera client, testes e exemplos |
| Componente "flexível" com 30 props | Componente com props tipadas e uso exemplificado | IA precisa saber o que cada prop faz |
| Helper mágico com short syntax | Função explícita com nome descritivo | Nomes explícitos guiam geração |
A regra prática: se um desenvolvedor novo precisasse de três reuniões para entender uma parte do sistema, a IA também vai precisar — e vai pior, porque ela não pode marcar reunião.
O frontend é onde a IA mais impressiona em demos e mais decepciona em produção. O motivo é o mesmo: geração de UI é sensível a contexto. Sem o design system, exemplos de uso e contrato de acessibilidade, o modelo reinventa padrões que já existiam no projeto.
O erro comum é tratar o design system como algo que existe apenas para o time. Em um time AI-native, o design system é o contexto primário para qualquer geração de UI. Tokens, regras de spacing, paleta, tipografia e padrões de composição devem ser entregues ao modelo de forma legível.
export const designTokens = {
color: {
background: {
primary: '#0b0f17',
secondary: '#111827',
elevated: '#1f2937',
},
foreground: {
primary: '#f9fafb',
muted: '#9ca3af',
},
accent: {
default: '#3b82f6',
hover: '#2563eb',
focus: '#60a5fa',
},
feedback: {
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
},
},
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
radius: { sm: 4, md: 8, lg: 12, pill: 9999 },
typography: {
fontFamily: "'Inter', system-ui, sans-serif",
size: { caption: 12, body: 14, subtitle: 16, title: 20, display: 28 },
weight: { regular: 400, medium: 500, semibold: 600 },
},
} as const;
export type DesignTokens = typeof designTokens;
Quando esse arquivo existe e é referenciado, o modelo não precisa adivinhar se a cor do botão de erro é #dc2626 ou #ef4444. Ele lê o token. Quando o prompt traz exemplos reais de uso, a qualidade do resultado dobra.
Componentes AI-readable seguem três regras:
variant="danger" é melhor que variant="d" ou kind={2}.Um exemplo de componente que um LLM consegue gerar, estender e testar:
import { designTokens } from './design-tokens';
export type ButtonProps = {
/** Texto exibido no botão. Obrigatório para garantir label acessível. */
label: string;
/** Variante visual. `primary` é a ação principal da tela; `ghost` é secundária. */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
/** Tamanho do botão. `sm` para tabelas, `md` para formulários, `lg` para CTAs. */
size?: 'sm' | 'md' | 'lg';
/** Estado de carregamento. Desabilita cliques e exibe spinner. */
isLoading?: boolean;
/** Callback disparado no clique. Não é chamado durante isLoading. */
onClick?: () => void;
/** Desabilita o botão de forma explícita. */
disabled?: boolean;
};
const sizeStyles: Record<NonNullable<ButtonProps['size']>, string> = {
sm: `padding: ${designTokens.spacing.sm}px ${designTokens.spacing.md}px;`,
md: `padding: ${designTokens.spacing.md}px ${designTokens.spacing.lg}px;`,
lg: `padding: ${designTokens.spacing.lg}px ${designTokens.spacing.xl}px;`,
};
export function Button({
label,
variant = 'primary',
size = 'md',
isLoading = false,
disabled = false,
onClick,
}: ButtonProps) {
const isDisabled = disabled || isLoading;
return (
<button
type="button"
disabled={isDisabled}
aria-busy={isLoading}
aria-label={isLoading ? `${label}, carregando` : label}
onClick={isDisabled ? undefined : onClick}
style={{
background: designTokens.color.background.primary,
color: designTokens.color.foreground.primary,
border: 'none',
borderRadius: designTokens.radius.md,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.6 : 1,
fontFamily: designTokens.typography.fontFamily,
fontSize: designTokens.typography.size.body,
...(sizeStyles[size] as React.CSSProperties),
}}
>
{isLoading ? 'Carregando...' : label}
</button>
);
}
Esse componente é AI-readable porque: props têm semântica clara (variant="danger" é autoexplicativo), JSDoc explica intenção, acessibilidade está no contrato (aria-busy, aria-label) e o comportamento com isLoading é determinístico. Quando um LLM recebe esse arquivo e recebe um pedido para criar um IconButton, ele replica o padrão. Quando recebe um componente "flexível" com 30 props sem JSDoc, ele chuta.
A prática que mais eleva qualidade de geração no frontend é spec-driven UI. Antes de pedir ao modelo para gerar um componente, escreva uma spec curta de comportamento:
## Spec: UserProfileCard
### Responsabilidades
- Exibir avatar, nome e papel do usuário.
- Mostrar status online/offline com indicador acessível.
- Oferecer ação "Enviar mensagem" quando o usuário não for o próprio.
### Acessibilidade
- Cartão é uma <article> com aria-label "Perfil de {nome}".
- Status tem role="status" e aria-label textual.
- Botão "Enviar mensagem" tem aria-label descritivo.
### Estados
- loading: esqueleto com aria-busy="true".
- error: mensagem com role="alert".
- empty: quando o usuário não existe, retorna null.
### Tokens obrigatórios
- background: tokens.background.elevated
- radius: tokens.radius.lg
- spacing interno: tokens.spacing.lg
Essa spec não é documento externo. É artefato de desenvolvimento. Quando anexada ao prompt, ela elimina 80% das idas e voltas de revisão. O componente gerado nasce dentro do design system, com acessibilidade e estados cobertos.
O fluxo abaixo mostra o loop que conecta spec, contexto, geração, revisão de acessibilidade, teste e entrega.
Cada caixa é observável. Se a auditoria de acessibilidade falha, você sabe onde corrigir. Se o visual diff diverge do design, o contexto montado estava incompleto. Sem esse loop explícito, o time acumula débito de UI que ninguém revisou.
No backend, o gargalo não é gerar uma rota a mais. É fazer com que a IA entenda o domínio sem precisar ler 50 arquivos. A solução é usar contratos verificáveis como fonte de verdade e expor ferramentas pequenas para agents consumirem.
A prática central é nunca deixar a API existir apenas como código de rota. O contrato deve ser um schema que gera validação, tipos, documentação e exemplos. Com zod, isso fica no mesmo arquivo da implementação:
import { z } from 'zod';
export const CreateOrderInput = z.object({
customerId: z.string().uuid('customerId deve ser um UUID válido'),
items: z
.array(
z.object({
sku: z.string().min(1),
quantity: z.number().int().positive('quantity deve ser inteiro positivo'),
}),
)
.min(1, 'items não pode ser vazio'),
couponCode: z.string().optional(),
});
export type CreateOrderInput = z.infer<typeof CreateOrderInput>;
export const OrderOutput = z.object({
id: z.string().uuid(),
status: z.enum(['pending', 'paid', 'shipped', 'canceled']),
totalCents: z.number().int().nonnegative(),
createdAt: z.string().datetime(),
});
export type OrderOutput = z.infer<typeof OrderOutput>;
Esse schema é a fonte de verdade. A partir dele, a IA gera o handler Express/Hono, o client tipado, os testes de caso extremo e a documentação OpenAPI. Se o schema fica sincronizado com o runtime, a IA nunca precisa adivinhar o formato do payload. A documentação deixa de ser um arquivo separado que envelhece e vira derivado do código.
Quando um agent de IA precisa chamar o seu backend, o caminho mais limpo não é um prompt genérico sobre "use a API REST". É expor a operação como uma tool Model Context Protocol. Uma tool MCP declara nome, descrição, schema de entrada, efeitos colaterais e contrato de saída. O agent não precisa descobrir a semântica do endpoint — ela vem embutida.
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const server = new McpServer({ name: 'orders-service', version: '1.0.0' });
const CreateOrderToolInput = z.object({
customerId: z.string().uuid(),
items: z
.array(
z.object({
sku: z.string().min(1),
quantity: z.number().int().positive(),
}),
)
.min(1),
});
server.tool(
'create_order',
'Cria um pedido para um cliente existente. Retorna o ID do pedido, status e total em centavos. Idempotente apenas se o header Idempotency-Key for enviado.',
CreateOrderToolInput.shape,
async (input) => {
const order = await createOrderInDomain(input);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({
id: order.id,
status: order.status,
totalCents: order.totalCents,
createdAt: order.createdAt.toISOString(),
}),
},
],
};
},
);
A descrição da tool não é enfeite. É o que o agent usa para decidir quando chamá-la. "Faz qualquer coisa com pedidos" gera decisões ruins; "Cria um pedido para um cliente existente, idempotente com Idempotency-Key" gera uso correto.
A tentação em projetos AI-native é escrever código "denso" que o modelo preenche. O efeito é o oposto: código denso, com efeitos colaterais escondidos, gera respostas erradas. Código legível, com funções pequenas e nomes explícitos, é o que produz melhor geração. Uncle Bob ainda vence.
type OrderItem = { sku: string; unitPriceCents: number; quantity: number };
type Coupon = { code: string; discountPercent: number };
export function computeOrderTotal(items: OrderItem[], coupon?: Coupon): number {
const subtotalCents = items.reduce((sum, item) => {
return sum + item.unitPriceCents * item.quantity;
}, 0);
const totalCents = applyCoupon(subtotalCents, coupon);
return totalCents;
}
function applyCoupon(subtotalCents: number, coupon?: Coupon): number {
if (!coupon) return subtotalCents;
if (coupon.discountPercent <= 0) return subtotalCents;
if (coupon.discountPercent > 100) {
throw new Error(`discountPercent inválido: ${coupon.discountPercent}`);
}
const discountCents = Math.round((subtotalCents * coupon.discountPercent) / 100);
return subtotalCents - discountCents;
}
Cada função faz uma coisa. O nome descreve a responsabilidade. O erro, quando existe, é explícito. Esse padrão é o que Andrej Karpathy defende: nada de macros, nada de sintaxe condensada, nada de otimização prematura. A IA lê isso e replica o padrão. Quando você escreve a mesma lógica em uma única linha com reduce aninhado, a IA replica o caos.
No backend, o loop gira em torno de validação de schema e contrato. Toda chamada de agent passa por seleção de tool, validação zod, execução e checagem de resultado.
A sequência torna explícito o que costuma ficar escondido: validação antes da execução, domínio puro depois, evidência ao final. Se o agent receber um erro de schema, ele sabe que o problema é a entrada — e a recuperação é corrigir argumentos, não aumentar a temperatura.
A semantic layer é a peça que conecta backend e contexto. Em vez de deixar o modelo ler todo o banco, você expõe consultas semânticas via tools. "Listar top 10 clientes por receita no trimestre" não é uma rota REST genérica; é uma tool MCP com schema, retorno tipado e permissão explícita.
| Camada | Responsável por | AI-native quando |
|---|---|---|
| API REST/HTTP | Integração com sistemas externos | Tem schema OpenAPI vivo |
| Tools MCP | Integração com agents | Cada tool tem descrição, schema, efeitos |
| Semantic layer | Consultas de negócio pré-definidas | Nomes legíveis por domínio, retorno tipado |
| Domain logic | Regras de negócio | Funções pequenas, sem cleverness |
Quando essas quatro camadas estão estruturadas, a IA opera em um espaço muito menor de decisões. Ela não reescreve SQL. Ela chama get_top_customers_by_revenue e lida com o resultado.
A tabela abaixo contrasta práticas de frontend e backend. O ponto não é jogar fora o que funcionava, mas reconhecer quando o padrão antigo gera fricção de geração.
| Camada | Tradicional | AI-native |
|---|---|---|
| Frontend: estilos | CSS ad-hoc, classes mágicas, !important | Design tokens como código, CSS modules, sem sobreposição |
| Frontend: componente | Lógica densa, HOCs, hooks encadeados | Props tipadas, JSDoc por prop, estados explícitos |
| Frontend: docs | Storybook desatualizado | Spec de comportamento + exemplos no repo |
| Frontend: a11y | Revisão manual no final | Auditoria automatizada no loop |
| Backend: API | Rota + exemplo no README | Schema zod/OpenAPI como fonte de verdade |
| Backend: integração com IA | Prompt genérico sobre a API | Tools MCP com contrato explícito |
| Backend: domínio | Função gigante com side effects | Funções pequenas, puras quando possível |
| Backend: observabilidade | Log textual para humanos | Eventos estruturados legíveis por agents |
A migração não precisa ser big bang. Escolha uma camada (geralmente API ou design system), estruture como AI-native e meça o ganho de geração.
Classes como .btn-1, .card-x ou estilos inline espalhados confundem o modelo. Quando ele vai gerar uma nova tela, replica o padrão mágico em vez de usar o token. O resultado é uma UI inconsistente que acumula débito.
Endpoints que aceitam any ou validam em camadas espalhadas geram respostas não tipadas. A IA infere o formato e produz client errado. A correção é mover o schema para o centro e derivar tudo dele.
Pedido vago como "faz um componente de perfil bonito" produz resultado genérico. Pedido com spec de estados, tokens obrigatórios e regras de acessibilidade produz resultado pronto para revisão. A diferença não é o modelo; é o contexto.
Quando o backend é um monolito sem comentários de domínio e sem divisão de funções, a IA precisa ler muito para decidir pouco. Cada chamada fica cara e sujeita a alucinação. A correção é isolar domínio em módulos pequenos com entradas e saídas tipadas.
Código denso, com currying, decorators e abstrações aninhadas, impressiona humanos e confunde IA. Em time AI-native, clareza vence inteligência. Se uma função pode ser escrita em 10 linhas legíveis em vez de 3 linhas densas, prefira 10.
Escolha uma tela e uma rota. Aplique o loop AI-native em pequena escala:
designTokens.ts. Se já existe, garanta que é a fonte de verdade.O objetivo não é converter o app inteiro. É provar para o time que o padrão AI-native reduz fricção e melhora qualidade. Quando isso fica claro, a migração vira consequência.
Codar AI-native não é escrever menos. É escrever de forma que a próxima linha seja gerada com evidência em vez de adivinhação. Isso exige disciplina em três frentes: contexto explícito (tokens, specs), contratos verificáveis (schemas, tools MCP) e domain logic legível (funções pequenas, nomes honestos).
O frontend AI-native trata o design system como fonte de contexto. Componentes têm props nomeadas, JSDoc e estados cobertos por spec. A acessibilidade deixa de ser revisão final e vira parte do contrato de geração. O backend AI-native coloca o schema no centro, expõe operações como tools MCP com descrições precisas e mantém o domínio puro o suficiente para qualquer leitor — humano ou modelo — entender.
A transição é gradual. Escolha uma camada, meça o ganho e expanda. O que não muda é o princípio: código AI-native é código que você mesmo gostaria de herdar — explícito nas intenções, econômico em surpresas, pródigo em evidência. Quem escreve assim ganha duas vezes: ganha um revisor humano mais rápido e ganha um copiloto de verdade.
O próximo artigo da série leva esses padrões para QA e DevOps. Vamos cobrir como avaliar loops de agents em produção, observar comportamento, configurar guardrails operacionais e medir custo, latência e segurança de side effects.
Continue exploring similar topics

Durante anos, o JavaScript foi tratado como uma solução “boa o suficiente” no backend. Ele funcionava, mas não oferecia garantias estruturais comparáveis a linguagens tradicionais de servidor como Java ou C#.

Um guia aprofundado sobre como a Fase 1 do Pos Tech FIAP forma engenheiros(as) front-end completos, unindo JavaScript avançado, TypeScript robusto, Design Systems, Next.js e fundamentos de arquitetura, performance e memória — da teoria à aplicação em produção.
Descubra quais são os frameworks de UI mais usados em 2025 — incluindo Tailwind CSS, Shadcn/UI, Material UI e outros — e entenda por que o CSS estático está substituindo o styled-components na era moderna do React.
A 47-point checklist to find bugs, security risks, and performance issues before launch.
Production-tested templates trusted by developers. Save weeks of setup on your next project.
