Em desenvolvimento tradicional (code-first), você escreve o código primeiro e — se sobrar tempo — documenta depois. O código é a “verdade”, e a documentação normalmente fica desatualizada. O Spec-Driven Development (SDD) inverte essa ordem: o contrato vem primeiro, formalizado em um padrão como OpenAPI, Protocol Buffers ou GraphQL Schema; o código implementa esse contrato; e ferramentas validam que a implementação respeita a especificação.
Neste tutorial avançado você vai aprender a aplicar SDD em projetos Salesforce com exemplos reais de Apex, OpenAPI e pipeline de validação: da escrita da spec à implementação de um REST Resource, passando por contract testing, mocks, geração de documentação e integração com Salesforce DX.

O que é Spec-Driven Development?
No SDD, a especificação é formalizada antes de qualquer código, usando padrões como:
- OpenAPI (Swagger) — para APIs REST
- Protocol Buffers — para serviços gRPC
- GraphQL Schema — para APIs GraphQL
O fluxo é:
- A especificação vem primeiro — o contrato é a fonte da verdade
- O código implementa a especificação
- Ferramentas validam que a implementação respeita o contrato
- A documentação é gerada automaticamente a partir da spec — e nunca fica obsoleta
Em uma frase: O contrato (spec) é a única fonte de verdade; o código é apenas uma implementação verificável desse contrato.
Spec-Driven vs TDD vs BDD
| Abordagem | Foco | Artefato primário | Quando usar |
|---|---|---|---|
| TDD | Comportamento de unidades de código | Testes unitários (verdes/vermelhos) | Lógica de negócio, classes Apex |
| BDD | Comportamento do usuário | Cenários Given/When/Then | Features orientadas a valor de negócio |
| SDD | Contrato entre sistemas | OpenAPI / GraphQL / Protobuf spec | APIs, integrações, contratos entre equipes |
O SDD complementa o TDD: a spec define o contrato externo; os testes TDD garantem que a implementação interna cumpre esse contrato.
Por que SDD importa agora (a era dos coding agents)
Com a aceleração do code generation por IA, o gargalo mudou: não é mais escrever código, é articular o que precisa ser construído. A qualidade da saída de um agente de IA depende diretamente da qualidade da especificação de entrada.
Na prática, equipes que adotam SDD relatam:
- ✅ Menos correção de curso constante com agentes de IA — a spec dá “senso comum” à tarefa
- ✅ O desenvolvedor passa de babysitter do agente para revisor de resultado
- ✅ Grandes tarefas sem spec exigem intervenção constante; com spec, o agente trabalha de forma autônoma
Como funciona na prática
O ciclo SDD
┌─────────────────────────────────────────────────────────────────┐
│ 1. DESIGN: Equipe define o contrato (OpenAPI YAML/JSON) │
│ - endpoints, métodos, schemas, erros, auth │
└─────────────────────────────────────────────────────────────────┘
│
▼ (revisão de contrato — antes de qualquer código!)
┌─────────────────────────────────────────────────────────────────┐
│ 2. VALIDATE: Spec é validada (lint, linting rules, preview) │
│ - Anypoint / Stoplight / Spectral │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. GENERATE: Ferramentas geram a partir da spec │
│ - documentação (ReDoc, Swagger UI) │
│ - mock server (Prism, WireMock) │
│ - SDKs / client stubs │
│ - schemas de validação │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. IMPLEMENT: Código Apex implementa o contrato │
│ - @RestResource / Apex REST │
│ - validação de payload contra schema │
│ - códigos de erro padronizados │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. CONTRACT TEST: CI valida implementação vs spec │
│ - schemathesis / Dredd / testes Apex │
│ - cada mudança de spec exige nova validação │
└─────────────────────────────────────────────────────────────────┘A regra de ouro do SDD
Nunca mude a implementação sem mudar a spec primeiro. Se o requisito mudou, atualize o contrato, valide, e só então altere o código.
Exemplo Prático: REST Resource Apex com OpenAPI
Vamos construir um exemplo completo: uma API de Pedidos (Orders) com:
POST /orders— criar pedidoGET /orders/{orderId}— buscar pedido por IDGET /orders?status=...— listar pedidos por statusPATCH /orders/{orderId}— atualizar parcialmente
Passo 1 — A especificação OpenAPI (a fonte da verdade)
Arquivo openapi/orders.yaml:
openapi: 3.0.3
info:
title: Orders API
version: 1.0.0
description: |
API de pedidos do Salesforce.
Contrato gerenciado via Spec-Driven Development.
servers:
- url: /services/apexrest
paths:
/orders:
post:
summary: Create a new order
operationId: createOrder
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderInput'
responses:
'201':
description: Order created
content:
application/json:
schema:
$ref: '#/components/schemas/OrderOutput'
'400':
description: Invalid payload
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
description: Forbidden
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
get:
summary: List orders by status
operationId: listOrders
parameters:
- name: status
in: query
required: false
schema:
type: string
enum: [Draft, Activated, Completed, Cancelled]
- name: limit
in: query
required: false
schema:
type: integer
default: 10
maximum: 100
responses:
'200':
description: List of orders
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/OrderOutput'
/orders/{orderId}:
get:
summary: Get order by ID
operationId: getOrder
parameters:
- name: orderId
in: path
required: true
schema:
type: string
responses:
'200':
description: Order found
content:
application/json:
schema:
$ref: '#/components/schemas/OrderOutput'
'404':
description: Order not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
patch:
summary: Partially update an order
operationId: updateOrder
parameters:
- name: orderId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderPatchInput'
responses:
'200':
description: Order updated
content:
application/json:
schema:
$ref: '#/components/schemas/OrderOutput'
'404':
description: Order not found
components:
schemas:
OrderInput:
type: object
required: [accountId, totalAmount, currencyIsoCode]
properties:
accountId:
type: string
description: Salesforce Account Id
orderNumber:
type: string
totalAmount:
type: number
format: decimal
currencyIsoCode:
type: string
pattern: '^[A-Z]{3}$'
description:
type: string
maxLength: 255
OrderPatchInput:
type: object
properties:
description:
type: string
maxLength: 255
totalAmount:
type: number
format: decimal
additionalProperties: false
OrderOutput:
type: object
properties:
id:
type: string
orderNumber:
type: string
accountId:
type: string
status:
type: string
enum: [Draft, Activated, Completed, Cancelled]
totalAmount:
type: number
currencyIsoCode:
type: string
createdDate:
type: string
format: date-time
ErrorResponse:
type: object
required: [code, message]
properties:
code:
type: string
example: INVALID_PAYLOAD
message:
type: string
example: 'Payload does not match schema'Por que a spec é valiosa aqui:
- O time de integração pode desenvolver em paralelo contra o contrato (mock server)
- A documentação interativa (Swagger UI) já nasce da spec
- O consumidor externo sabe exatamente o que esperar — campos obrigatórios, enums, erros
- Mudanças de contrato viram pull requests na spec — revisáveis e auditáveis
Passo 2 — Implementação Apex (respeitando o contrato)
Note como cada método reflete exatamente uma operationId da spec.
/**
* @description REST Resource for Orders API.
* Contract: openapi/orders.yaml (source of truth).
* Do NOT change behavior without updating the spec first.
*/
@RestResource(urlMapping='/orders/*')
global with sharing class OrdersAPI {
/**
* operationId: createOrder
* POST /orders
*/
@HttpPost
global static void createOrder() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
try {
// 1. Parse raw payload
Object raw = JSON.deserializeUntyped(req.requestBody.toString());
// 2. Validate against the contract's OrderInput schema
OrderInput input = OrderInput.fromJson(req.requestBody.toString());
List<String> errors = input.validate();
if (!errors.isEmpty()) {
sendError(res, 400, 'INVALID_PAYLOAD', String.join(errors, '; '));
return;
}
// 3. Business logic (delegated to service layer)
Order__c order = OrderService.createOrder(input);
// 4. Response exactly as spec: 201 + OrderOutput
res.statusCode = 201;
res.responseBody = Blob.valueOf(
JSON.serialize(OrderOutput.fromSObject(order))
);
} catch (JSONException je) {
sendError(res, 400, 'INVALID_PAYLOAD', 'Malformed JSON body.');
} catch (DmlException de) {
sendError(res, 400, 'DML_FAILED', de.getDmlMessage(0));
} catch (Exception e) {
sendError(res, 500, 'INTERNAL_ERROR', 'Unexpected server error.');
}
}
/**
* operationId: getOrder
* GET /orders/{orderId}
*/
@HttpGet
global static void getOrder() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
String orderId = req.requestURI.substringAfterLast('/');
if (!isValidId(orderId)) {
sendError(res, 400, 'INVALID_ID', 'Order Id is not a valid Salesforce Id.');
return;
}
Order__c order = OrderService.getOrderById(orderId);
if (order == null) {
sendError(res, 404, 'NOT_FOUND', 'Order not found: ' + orderId);
return;
}
res.statusCode = 200;
res.responseBody = Blob.valueOf(JSON.serialize(OrderOutput.fromSObject(order)));
}
/**
* operationId: listOrders
* GET /orders?status=Activated&limit=10
*/
@HttpGet
global static void listOrders() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
String status = req.params.get('status');
Integer limit = parseLimit(req.params.get('limit')); // default 10, max 100
List<Order__c> orders = OrderService.listOrders(status, limit);
List<OrderOutput> outputs = new List<OrderOutput>();
for (Order__c o : orders) {
outputs.add(OrderOutput.fromSObject(o));
}
res.statusCode = 200;
res.responseBody = Blob.valueOf(JSON.serialize(outputs));
}
/**
* operationId: updateOrder
* PATCH /orders/{orderId}
*/
@HttpPatch
global static void updateOrder() {
RestRequest req = RestContext.request;
RestResponse res = RestContext.response;
String orderId = req.requestURI.substringAfterLast('/');
if (!isValidId(orderId)) {
sendError(res, 400, 'INVALID_ID', 'Order Id is not a valid Salesforce Id.');
return;
}
OrderPatchInput patch = OrderPatchInput.fromJson(req.requestBody.toString());
List<String> errors = patch.validate();
if (!errors.isEmpty()) {
sendError(res, 400, 'INVALID_PAYLOAD', String.join(errors, '; '));
return;
}
Order__c updated = OrderService.updateOrder(orderId, patch);
if (updated == null) {
sendError(res, 404, 'NOT_FOUND', 'Order not found: ' + orderId);
return;
}
res.statusCode = 200;
res.responseBody = Blob.valueOf(JSON.serialize(OrderOutput.fromSObject(updated)));
}
// ============ Helpers ============
private static void sendError(RestResponse res, Integer code, String errorCode, String message) {
res.statusCode = code;
res.responseBody = Blob.valueOf(JSON.serialize(
new Map<String, Object>{
'code' => errorCode,
'message' => message
}
));
}
private static Boolean isValidId(String id) {
// Contract requires 15 or 18 char Salesforce Id
return String.isNotBlank(id)
&& (id.length() == 15 || id.length() == 18)
&& id.isAlphanumeric();
}
private static Integer parseLimit(String raw) {
Integer limit = 10; // spec default
if (String.isNotBlank(raw)) {
try {
limit = Integer.valueOf(raw);
} catch (TypeException te) {
limit = 10;
}
}
// spec maximum: 100
return limit > 100 ? 100 : limit;
}
}Passo 3 — Classes de contrato (DTOs com validação derivada da spec)
/**
* @description Contract DTO for OrderInput schema (openapi/orders.yaml).
* Fields mirror the spec exactly — rename here = breaking change.
*/
public class OrderInput {
public String accountId;
public String orderNumber;
public Decimal totalAmount;
public String currencyIsoCode;
public String description;
public static OrderInput fromJson(String json) {
return (OrderInput) JSON.deserialize(json, OrderInput.class);
}
/**
* Validation rules derived from the OpenAPI schema:
* - required: accountId, totalAmount, currencyIsoCode
* - pattern: currencyIsoCode = ^[A-Z]{3}$
* - maxLength: description = 255
*/
public List<String> validate() {
List<String> errors = new List<String>();
if (String.isBlank(accountId)) {
errors.add('accountId is required');
}
if (totalAmount == null) {
errors.add('totalAmount is required');
}
if (String.isBlank(currencyIsoCode)) {
errors.add('currencyIsoCode is required');
} else if (!Pattern.matches('^[A-Z]{3}$', currencyIsoCode)) {
errors.add('currencyIsoCode must match ^[A-Z]{3}$');
}
if (description != null && description.length() > 255) {
errors.add('description must be at most 255 characters');
}
return errors;
}
}/**
* @description Contract DTO for OrderPatchInput schema.
* additionalProperties: false → unknown fields rejected.
*/
public class OrderPatchInput {
public String description;
public Decimal totalAmount;
public static OrderPatchInput fromJson(String json) {
// Reject unknown fields to honor additionalProperties: false
Map<String, Object> raw = (Map<String, Object>) JSON.deserializeUntyped(json);
Set<String> allowed = new Set<String>{'description', 'totalAmount'};
for (String key : raw.keySet()) {
if (!allowed.contains(key)) {
throw new JSONException('Unknown field in payload: ' + key);
}
}
return (OrderPatchInput) JSON.deserialize(json, OrderPatchInput.class);
}
public List<String> validate() {
List<String> errors = new List<String>();
if (description != null && description.length() > 255) {
errors.add('description must be at most 255 characters');
}
return errors;
}
}/**
* @description Contract DTO for OrderOutput schema.
* Serialization shape must match the spec exactly.
*/
public class OrderOutput {
public String id;
public String orderNumber;
public String accountId;
public String status;
public Decimal totalAmount;
public String currencyIsoCode;
public String createdDate;
public static OrderOutput fromSObject(Order__c o) {
OrderOutput out = new OrderOutput();
out.id = o.Id;
out.orderNumber = o.Order_Number__c;
out.accountId = o.Account__c;
out.status = o.Status__c;
out.totalAmount = o.Total_Amount__c;
out.currencyIsoCode = o.CurrencyIsoCode;
out.createdDate = o.CreatedDate != null
? o.CreatedDate.format('yyyy-MM-dd'T'HH:mm:ss'Z'')
: null;
return out;
}
}Passo 4 — Contract tests em Apex
Os testes verificam a implementação contra o contrato, não apenas contra a lógica:
/**
* @description Contract tests for Orders API.
* Each test pins a behavior declared in openapi/orders.yaml.
*/
@IsTest
private class OrdersAPITest {
@IsTest
static void createOrder_returns201_and_matchesOrderOutputSchema() {
// Setup: insert Account
Account acc = new Account(Name = 'Test Account');
insert acc;
// Build request exactly per spec
RestRequest req = new RestRequest();
req.httpMethod = 'POST';
req.requestURI = '/services/apexrest/orders';
req.requestBody = Blob.valueOf(JSON.serialize(new Map<String, Object>{
'accountId' => acc.Id,
'totalAmount' => 150.50,
'currencyIsoCode' => 'BRL',
'description' => 'Pedido de teste'
}));
RestContext.request = req;
RestContext.response = new RestResponse();
Test.startTest();
OrdersAPI.createOrder();
Test.stopTest();
// Assert status code from spec
System.assertEquals(201, RestContext.response.statusCode);
// Assert response shape (OrderOutput)
Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(
RestContext.response.responseBody.toString()
);
System.assert(body.containsKey('id'));
System.assert(body.containsKey('orderNumber'));
System.assert(body.containsKey('status'));
System.assertEquals('BRL', body.get('currencyIsoCode'));
}
@IsTest
static void createOrder_missingRequiredField_returns400() {
// No accountId → must fail per spec (required)
RestRequest req = new RestRequest();
req.httpMethod = 'POST';
req.requestURI = '/services/apexrest/orders';
req.requestBody = Blob.valueOf(JSON.serialize(new Map<String, Object>{
'totalAmount' => 100.00,
'currencyIsoCode' => 'USD'
}));
RestContext.request = req;
RestContext.response = new RestResponse();
Test.startTest();
OrdersAPI.createOrder();
Test.stopTest();
System.assertEquals(400, RestContext.response.statusCode);
Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(
RestContext.response.responseBody.toString()
);
System.assertEquals('INVALID_PAYLOAD', body.get('code'));
}
@IsTest
static void getOrder_unknownId_returns404() {
RestRequest req = new RestRequest();
req.httpMethod = 'GET';
req.requestURI = '/services/apexrest/orders/001000000000000AAA';
RestContext.request = req;
RestContext.response = new RestResponse();
Test.startTest();
OrdersAPI.getOrder();
Test.stopTest();
System.assertEquals(404, RestContext.response.statusCode);
Map<String, Object> body = (Map<String, Object>) JSON.deserializeUntyped(
RestContext.response.responseBody.toString()
);
System.assertEquals('NOT_FOUND', body.get('code'));
}
@IsTest
static void patchOrder_unknownField_returns400() {
// additionalProperties: false → unknown field rejected
RestRequest req = new RestRequest();
req.httpMethod = 'PATCH';
req.requestURI = '/services/apexrest/orders/001000000000000AAA';
req.requestBody = Blob.valueOf('{"hackedField": "x"}');
RestContext.request = req;
RestContext.response = new RestResponse();
Test.startTest();
OrdersAPI.updateOrder();
Test.stopTest();
System.assertEquals(400, RestContext.response.statusCode);
}
@IsTest
static void listOrders_invalidStatus_returnsEmptyList() {
// status enum is [Draft, Activated, Completed, Cancelled]
RestRequest req = new RestRequest();
req.httpMethod = 'GET';
req.requestURI = '/services/apexrest/orders';
req.addParameter('status', 'InvalidStatus');
RestContext.request = req;
RestContext.response = new RestResponse();
Test.startTest();
OrdersAPI.listOrders();
Test.stopTest();
// Contract says: invalid enum → filter matches nothing
System.assertEquals(200, RestContext.response.statusCode);
}
}Ferramentas para o fluxo SDD em Salesforce
1. Validação e lint da spec
| Ferramenta | Uso |
|---|---|
| Spectral (Stoplight) | Lint de OpenAPI com rulesets customizadas (naming, required fields, error codes) |
| Redocly CLI | Lint + build de documentação a partir da spec |
| Anypoint Design Center (MuleSoft) | Design visual de APIs, validação OAS, mocking — integrado ao ecossistema Salesforce |
| swagger-cli | Validação estrutural e bundle de multi-arquivos |
Exemplo de lint com Spectral:
# Install
npm install -g @stoplight/spectral-cli
# Lint the contract in CI
spectral lint openapi/orders.yaml --ruleset .spectral.yml
# Output
# 0 problems (0 errors, 0 warnings)Ruleset customizado (.spectral.yml):
extends: ["spectral:oas"]
rules:
# Toda resposta de erro deve ter o shape ErrorResponse
error-response-schema:
given: "$.paths[*][*].responses[4??][*].content[*].schema"
then:
field: "$ref"
function: pattern
functionOptions:
match: "#/components/schemas/ErrorResponse"
# Todo parâmetro de path deve ter um exemplo
path-parameters-have-example:
given: "$.paths[*].parameters[?(@.in=='path')]"
then:
field: example
function: defined2. Mock servers para desenvolvimento paralelo
# Prism (Stoplight) — mock a partir da spec
npx @stoplight/prism-cli mock openapi/orders.yaml
# Testa o mock
curl -X POST http://localhost:4010/orders -H 'Content-Type: application/json' -d '{"accountId":"001xx","totalAmount":100,"currencyIsoCode":"USD"}'
# Resposta mock respeitando o schema
# {"id":"...","orderNumber":"...","status":"Draft",...}3. Contract testing automatizado (CI)
| Ferramenta | O que faz |
|---|---|
| Dredd | Executa cada endpoint da spec contra a implementação e compara respostas com o contrato |
| Schemathesis | Property-based testing: gera payloads aleatórios e valida contra o schema |
| Apex Test Suite | Testes Apex que pinam o shape (como no exemplo acima) |
| Postman / Newman | Collections geradas da spec, rodadas no CI |
Exemplo com Dredd em pipeline GitHub Actions:
name: contract-test
on: [pull_request]
jobs:
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint spec
run: npx @stoplight/spectral-cli lint openapi/orders.yaml
- name: Contract test
env:
SALESFORCE_INSTANCE_URL: ${{ secrets.SF_INSTANCE_URL }}
SALESFORCE_TOKEN: ${{ secrets.SF_ACCESS_TOKEN }}
run: |
npx dredd openapi/orders.yaml http://$SALESFORCE_INSTANCE_URL/services/apexrest --server: https://$SALESFORCE_INSTANCE_URL --headers "Authorization: Bearer $SALESFORCE_TOKEN"4. Geração de clientes e documentação
# Swagger UI / ReDoc (documentação interativa)
npx redocly build-docs openapi/orders.yaml -o docs/orders.html
# Geração de SDK cliente (ex: TypeScript)
npx openapi-generator-cli generate -i openapi/orders.yaml -g typescript-axios -o ./generated/orders-sdkWorkflow SDD com Salesforce DX
Estrutura de repositório recomendada:
my-salesforce-project/
├── force-app/
│ └── main/
│ └── default/
│ ├── classes/
│ │ ├── OrdersAPI.cls
│ │ ├── OrdersAPITest.cls
│ │ ├── OrderInput.cls
│ │ ├── OrderOutput.cls
│ │ └── OrderService.cls
│ └── objects/
│ └── Order__c/
├── openapi/ ← a fonte da verdade
│ └── orders.yaml
├── docs/
│ └── orders.html ← gerado da spec
├── .spectral.yml ← regras de lint do contrato
├── sfdx-project.json
└── .github/
└── workflows/
└── contract-test.yml ← CI: lint + contract testRegras do time (Definition of Done do SDD)
- ☐ Toda mudança de comportamento começa com um PR na spec
- ☐ A spec passa no Spectral lint (CI)
- ☐ O mock server reflete a nova spec
- ☐ A implementação Apex passa nos contract tests
- ☐ A documentação gerada foi atualizada
- ☐ Breaking changes foram comunicados aos consumidores antes do deploy
Armadilhas comuns e como evitá-las
| Armadilha | Consequência | Solução |
|---|---|---|
| Spec escrita depois do código | Vira documentação de luxo, não contrato | Spec primeiro no fluxo; bloqueie PR de código sem spec |
| Spec muito detalhada demais | Lento de manter, ninguém revisa | Spec no nível de contrato; detalhes de implementação ficam fora |
Ignorar o additionalProperties | Consumidores mandam campos extras que quebram o parser | Definir explicitamente e validar no DTO |
| Erros fora do shape da spec | Consumidor não consegue tratar falhas | Padronizar ErrorResponse em toda a API |
| Deploy de breaking change sem aviso | Integrações quebram em produção | Versionar a spec (/v1/orders); deprecar antes de remover |
| Não testar o contrato no CI | Spec e código divergem silenciosamente | Dredd/Schemathesis + testes Apex no pipeline |
SDD + Agentes de IA (Agentforce, coding agents)
O SDD ganha ainda mais relevância com agentes de IA:
- Especificações como prompts de alta qualidade — uma spec OpenAPI bem escrita é o melhor “prompt” para um agente implementar o endpoint
- Validação objetiva — o contract test é o critério de aceite imparcial: o código do agente passa ou não passa, sem subjetividade
- Agentes consumindo APIs — APIs com spec OpenAPI podem ser expostas a agentes (via MCP ou tools) com contrato claro de entrada/saída
- llms.txt para documentação — exponha a spec e docs em formato legível para LLMs manterem o conhecimento de modelo atualizado
Padrão recomendado para features com agentes:
1. Escreva a spec (contrato) — humanos revisam
2. Gere o esqueleto de implementação com o agente
3. Rode os contract tests — o agente corrige até passar
4. Review humano final + deployChecklist final
- ☐ Spec OpenAPI escrita e revisada ANTES do código
- ☐ Spec passa no lint (Spectral/Redocly) no CI
- ☐ Mock server disponível para consumidores
- ☐ DTOs Apex espelham os schemas da spec (mesmos nomes!)
- ☐ Validação de required/pattern/maxLength derivada da spec
- ☐ Códigos de erro padronizados (ErrorResponse)
- ☐ Contract tests rodando no CI (Dredd/Schemathesis + Apex)
- ☐ Documentação gerada da spec está publicada
- ☐ Processo de breaking change definido (versionamento)
- ☐ Time alinhado: spec primeiro, código depois
Conceitos aplicados
| Conceito | Onde se aplica |
|---|---|
| OpenAPI Specification | Formato do contrato (YAML) — endpoints, schemas, erros, auth |
| Contract-First | Ordem: spec → validação → geração → implementação → teste |
| Mock Server | Prism/WireMock permitem desenvolvimento paralelo |
| Contract Testing | Dredd/Schemathesis garantem que código respeita a spec |
| DTO / Schema Mapping | Classes Apex espelham schemas OpenAPI |
additionalProperties | Controle estrito de campos aceitos no payload |
| CI/CD Pipeline | Lint da spec + contract tests em todo PR |
| API Versioning | /v1/ na spec para evoluir sem quebrar consumidores |
Referências
- OpenAPI Specification (OAS 3.0)
- Trailhead — API Design with MuleSoft (Specification-Driven Design)
- Salesforce Developers — Apex REST
- Spectral — Lint de OpenAPI
- Dredd — Contract testing
- Schemathesis — Property-based API testing
- Anypoint Design Center (MuleSoft)
- Apideck — Introduction to spec-driven API development
