On this page

🔗The Problem with Stale Mocks

Here's the scenario every developer team faces: you've set up a beautiful mock server using your upstream API's OpenAPI spec. Your frontend team is unblocked. Your tests are green. Everything is working perfectly.

Then, three weeks later, the backend team ships a change. Maybe they added a new field to the user response. Maybe they changed the format of a date field. Maybe they introduced a breaking change that you won't discover until integration testing catches it.

Your mocks are now stale. They're lying to you. They're giving you false confidence that everything is working when it's actually broken.

Manual mock maintenance is a time sink. Someone has to remember to periodically check the upstream API, compare responses, and update fixtures. In practice, this doesn't happen. Mocks drift out of sync, and you discover the problem when it's too late—usually during a release or when a customer reports a bug.

🔗The Solution: Automatic Change Detection

MockForge's automatic API sync feature solves this by continuously polling upstream APIs, comparing responses with recorded fixtures, and detecting changes automatically. It's a background service that runs silently, keeping your mocks synchronized with reality without any manual intervention.

The key insight here is that we don't need to regenerate mocks from scratch every time. We just need to know when something changed, and optionally update the fixtures automatically. This is exactly what MockForge's sync service does.

🔗How It Works: The Architecture

The sync service is built around a simple, powerful pattern:

graph TD
    A[Sync Service] -->|Periodic Timer| B[Fetch Recent Requests]
    B -->|For Each Request| C[Replay to Upstream]
    C -->|Get Response| D[Deep Comparison]
    D -->|Changes Detected?| E[Update Fixture]
    D -->|No Changes| F[Log Status]
    E --> G[Notify/Log]
    F --> G

The service runs in the background, periodically (default: every hour) fetching recent recorded requests from the database and replaying them to the upstream API. Each response is compared against the recorded fixture using deep response comparison. If changes are detected, the fixture is optionally updated automatically.

🔗Deep Response Comparison: The Core Algorithm

The comparison algorithm is where the real intelligence lives. It's not just a simple string comparison—it's a content-aware diff engine that understands JSON structure, HTTP semantics, and which differences actually matter.

Here's how it works:

// Simplified comparison logic
pub fn compare(
    original_status: i32,
    original_headers: &HashMap<String, String>,
    original_body: &[u8],
    current_status: i32,
    current_headers: &HashMap<String, String>,
    current_body: &[u8],
) -> ComparisonResult {
    let mut differences = Vec::new();
    
    // Compare status codes
    if original_status != current_status {
        differences.push(Difference::status_changed(original_status, current_status));
    }
    
    // Compare headers (ignoring dynamic ones like 'date', 'x-request-id')
    let header_diffs = compare_headers(original_headers, current_headers);
    differences.extend(header_diffs);
    
    // Compare bodies based on content type
    let body_diffs = compare_bodies(original_body, current_body, content_type);
    differences.extend(body_diffs);
    
    ComparisonResult { matches: differences.is_empty(), differences }
}

The comparison is smart about what to ignore. Dynamic headers like date, x-request-id, and set-cookie are automatically excluded because they change on every request and aren't meaningful differences. The body comparison is content-aware: JSON bodies get deep structural comparison, while text bodies use line-by-line diffing.

🔗JSON Body Comparison: Deep Structural Diffing

For JSON responses, the comparison performs a recursive structural diff:

fn compare_json_values(original: &Value, current: &Value, path: &str) -> Vec<Difference> {
    match (original, current) {
        (Value::Object(orig_map), Value::Object(curr_map)) => {
            // Check for removed or changed keys
            for (key, orig_value) in orig_map {
                let new_path = format!("{}.{}", path, key);
                match curr_map.get(key) {
                    Some(curr_value) => {
                        // Recursively compare nested objects
                        differences.extend(compare_json_values(orig_value, curr_value, &new_path));
                    }
                    None => {
                        differences.push(Difference::removed(&new_path, orig_value));
                    }
                }
            }
            
            // Check for added keys
            for (key, curr_value) in curr_map {
                if !orig_map.contains_key(key) {
                    differences.push(Difference::added(&format!("{}.{}", path, key), curr_value));
                }
            }
        }
        // Handle arrays, type changes, value changes...
    }
}

This gives you precise information about what changed: which fields were added, removed, or modified, and where in the JSON structure. The path tracking makes it easy to understand the impact: is this a breaking change in a critical field, or just a new optional field being added?

🔗Configuration: Flexible Sync Behavior

The sync service is highly configurable to match your workflow:

recorder:
  sync:
    enabled: true
    upstream_url: "https://api.example.com"
    interval_seconds: 3600  # 1 hour default
    auto_update: true        # Automatically update fixtures when changes detected
    max_requests_per_sync: 100
    request_timeout_seconds: 30
    sync_get_only: true      # Only sync GET requests (safer)
    headers:
      Authorization: "Bearer ${API_TOKEN}"

The sync_get_only flag is particularly important. By default, the sync service only replays GET requests. This is safer because GET requests are idempotent—they don't modify state on the upstream server. You can enable syncing for all methods, but be careful: replaying POST or PUT requests could create duplicate resources or modify data.

The auto_update flag controls whether fixtures are automatically updated when changes are detected. If false, the service will only detect and report changes without modifying your fixtures. This gives you the option to review changes before accepting them.

🔗Observability: Knowing What Changed

The sync service provides comprehensive status and change tracking:

pub struct SyncStatus {
    pub is_running: bool,
    pub last_sync: Option<DateTime<Utc>>,
    pub last_changes_detected: usize,
    pub last_fixtures_updated: usize,
    pub last_error: Option<String>,
    pub total_syncs: u64,
}

This status is exposed via API endpoints (/api/recorder/sync/status, /api/recorder/sync/changes), allowing you to monitor sync health and view change history. Each detected change includes:

  • The request method and path
  • The comparison result with detailed differences
  • Whether the fixture was updated
  • A timestamp of when the change was detected

🔗Production Patterns: When to Use Auto-Sync

Auto-sync is powerful, but it's not always the right choice. Here are the patterns:

Use auto-sync when:

  • You're mocking a stable, well-documented API
  • The upstream API changes are infrequent and usually additive
  • You want early detection of breaking changes
  • Your team is comfortable with fixtures being automatically updated

Don't use auto-sync when:

  • The upstream API is unstable or frequently changing
  • Changes are often breaking and you need manual review
  • You're using auto-sync for POST/PUT/DELETE requests (risk of side effects)
  • You need strict control over fixture updates

Hybrid approach: Enable auto-sync with auto_update: false. This gives you automatic change detection without automatic fixture updates. Review the changes via the API, then manually trigger fixture updates for approved changes.

🔗Cost Considerations

The sync service makes HTTP requests to your upstream API. The cost here is primarily network overhead and API rate limits, not compute. The service is designed to be efficient:

  • It only syncs recent requests (configurable via max_requests_per_sync)
  • It respects timeouts and doesn't block on slow upstream responses
  • It can be configured to run less frequently if upstream API rate limits are a concern

For most use cases, syncing 100 requests once per hour is negligible. If you're hitting rate limits, increase the interval_seconds or decrease max_requests_per_sync.

🔗Manual Sync Triggers

Sometimes you need to sync immediately, not wait for the next scheduled interval. The sync service exposes a manual trigger endpoint:

POST /api/recorder/sync/now

This immediately performs a sync operation and returns the detected changes. Useful for:

  • Testing sync configuration
  • Triggering syncs after known upstream deployments
  • Validating sync behavior before enabling automatic updates

🔗The Bottom Line

Automatic API sync transforms mock maintenance from a manual, error-prone process into an automated, observable system. It's not magic—it's a well-designed background service that continuously validates your mocks against reality and optionally keeps them in sync.

The deep response comparison algorithm is the key differentiator. It's not just detecting that something changed; it's telling you exactly what changed and where, giving you the information you need to make informed decisions about fixture updates.

For teams building against upstream APIs that change over time, this feature eliminates an entire class of "mock drift" bugs. It's the difference between mocks that are a liability and mocks that are a reliable development tool.