Spec-Driven Development em Salesforce

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 é:

  1. A especificação vem primeiro — o contrato é a fonte da verdade
  2. O código implementa a especificação
  3. Ferramentas validam que a implementação respeita o contrato
  4. 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

AbordagemFocoArtefato primárioQuando usar
TDDComportamento de unidades de códigoTestes unitários (verdes/vermelhos)Lógica de negócio, classes Apex
BDDComportamento do usuárioCenários Given/When/ThenFeatures orientadas a valor de negócio
SDDContrato entre sistemasOpenAPI / GraphQL / Protobuf specAPIs, 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 pedido
  • GET /orders/{orderId} — buscar pedido por ID
  • GET /orders?status=... — listar pedidos por status
  • PATCH /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

FerramentaUso
Spectral (Stoplight)Lint de OpenAPI com rulesets customizadas (naming, required fields, error codes)
Redocly CLILint + 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-cliValidaçã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: defined

2. 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)

FerramentaO que faz
DreddExecuta cada endpoint da spec contra a implementação e compara respostas com o contrato
SchemathesisProperty-based testing: gera payloads aleatórios e valida contra o schema
Apex Test SuiteTestes Apex que pinam o shape (como no exemplo acima)
Postman / NewmanCollections 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-sdk

Workflow 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 test

Regras do time (Definition of Done do SDD)

  1. ☐ Toda mudança de comportamento começa com um PR na spec
  2. ☐ A spec passa no Spectral lint (CI)
  3. ☐ O mock server reflete a nova spec
  4. ☐ A implementação Apex passa nos contract tests
  5. ☐ A documentação gerada foi atualizada
  6. ☐ Breaking changes foram comunicados aos consumidores antes do deploy

Armadilhas comuns e como evitá-las

ArmadilhaConsequênciaSolução
Spec escrita depois do códigoVira documentação de luxo, não contratoSpec primeiro no fluxo; bloqueie PR de código sem spec
Spec muito detalhada demaisLento de manter, ninguém revisaSpec no nível de contrato; detalhes de implementação ficam fora
Ignorar o additionalPropertiesConsumidores mandam campos extras que quebram o parserDefinir explicitamente e validar no DTO
Erros fora do shape da specConsumidor não consegue tratar falhasPadronizar ErrorResponse em toda a API
Deploy de breaking change sem avisoIntegrações quebram em produçãoVersionar a spec (/v1/orders); deprecar antes de remover
Não testar o contrato no CISpec e código divergem silenciosamenteDredd/Schemathesis + testes Apex no pipeline

SDD + Agentes de IA (Agentforce, coding agents)

O SDD ganha ainda mais relevância com agentes de IA:

  1. Especificações como prompts de alta qualidade — uma spec OpenAPI bem escrita é o melhor “prompt” para um agente implementar o endpoint
  2. Validação objetiva — o contract test é o critério de aceite imparcial: o código do agente passa ou não passa, sem subjetividade
  3. Agentes consumindo APIs — APIs com spec OpenAPI podem ser expostas a agentes (via MCP ou tools) com contrato claro de entrada/saída
  4. 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 + deploy

Checklist 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

ConceitoOnde se aplica
OpenAPI SpecificationFormato do contrato (YAML) — endpoints, schemas, erros, auth
Contract-FirstOrdem: spec → validação → geração → implementação → teste
Mock ServerPrism/WireMock permitem desenvolvimento paralelo
Contract TestingDredd/Schemathesis garantem que código respeita a spec
DTO / Schema MappingClasses Apex espelham schemas OpenAPI
additionalPropertiesControle estrito de campos aceitos no payload
CI/CD PipelineLint da spec + contract tests em todo PR
API Versioning/v1/ na spec para evoluir sem quebrar consumidores

Referências

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *