On this page
- The Problem with Stale Mocks
- The Solution: Automatic Change Detection
- How It Works: The Architecture
- Deep Response Comparison: The Core Algorithm
- JSON Body Comparison: Deep Structural Diffing
- Configuration: Flexible Sync Behavior
- Observability: Knowing What Changed
- Production Patterns: When to Use Auto-Sync
- Cost Considerations
- Manual Sync Triggers
- The Bottom Line
🔗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.