Skip to main content

Command Palette

Search for a command to run...

API Live Sync #7: import-export

Published
11 min read
API Live Sync #7: import-export

How we built notifications, team collaboration, and framework-specific optimizations that make live API sync come together a little more.


Previously in our Live Sync implementation...

In our previous posts, we laid the foundation for live API synchronization with sync engines, setup wizards, and real-time status indicators. In the end, we had a working system that could detect changes and update collections automatically.

But real-world development is messier than our initial implementation assumed. Teams work together, frameworks have…uhm…peculiarities, and developers need to know what's happening when things change. Today, we're diving into the advanced features that transform our live sync system from "functional" to "usable."

🔗 Follow along: All code is available in this repository.

The Challenge

Building a sync system for a single developer is one thing. Building one that works seamlessly for teams, handles framework-specific quirks, and provides notifications is something else. I didn’t know this when I embarked on this task. Took me longer than the other parts to implement and write about. We needed to solve four critical challenges:

  1. How do we notify users about changes without being annoying? (Change Notifications)

  2. How do we integrate with existing workflows? (Import/Export Integration)

  3. How do teams collaborate without stepping on each other and starting a war? (Team Collaboration)

  4. How do we handle framework-specific peculiarities? (Framework Optimizations)

Let's dive into how we solved each of these challenges.


Change Notification System

The Problem: How much information is too much or too little?

Notifications are like seasoning in cooking - too little and your users are left wondering what happened, too much and you've ruined the experience. We needed a system that could:

  • Show toast notifications for immediate feedback

  • Maintain a notification history for later review

  • Highlight changes in the UI without being intrusive

  • Provide actionable information, not just status updates

The Solution

We built a notification system with three layers: toasts for immediate feedback, persistent notifications for history, and visual highlights for in-context awareness. I started by trying to solve the immediate feedback problem, as developers want to know what’s happening, when it happens. Then decided to persist because they might be too busy. The visual highlights were like icing on the cake.

1. The Change Notification Service

class ChangeNotificationService {
  private notifications = reactive<ChangeNotification[]>([])
  private toasts = reactive<ToastNotification[]>([])
  private undoHistory = reactive<Map<string, any>>(new Map())

  /**
   * Show toast notification for sync events
   */
  showSyncToast(
    source: LiveSpecSource,
    changes: SpecDiff,
    success: boolean,
    error?: string
  ): void {
    if (success) {
      const changeCount = this.getChangeCount(changes)
      const frameworkName = source.framework || "API"
      const message =
        changeCount > 0
          ? `${changeCount} endpoint${changeCount === 1 ? "" : "s"} updated from ${frameworkName} code`
          : "API is up to date."

      this.showToast({
        type: "success",
        title: "Sync Complete",
        message,
        actions:
          changeCount > 0
            ? [
                {
                  label: "View Changes",
                  action: () => this.showChangeDiff(source, changes),
                },
              ]
            : undefined,
      })
    }
  }
}

This service is like a smart guy who knows when to speak up and when to stay quiet. It tracks the context of changes and provides relevant actions, not just passive information.

2. Smart Toast Notifications: Contextual and Actionable

The key insight was making toasts actionable rather than just a regular notification:

// Success toast with action
this.showToast({
  type: "success",
  title: "Sync Complete",
  message: `${changeCount} endpoints updated from ${frameworkName} code`,
  actions: [
    {
      label: "View Changes",
      action: () => this.showChangeDiff(source, changes),
    },
  ],
})

// Error toast with recovery action
this.showToast({
  type: "error",
  title: "Sync Failed",
  message: error || "Failed to sync with development server",
  actions: [
    {
      label: "Retry",
      action: () => this.retrySyncForSource(source),
    },
  ],
})

Instead of just saying "something happened," we tell users what happened and what they can do about it.

3. Breaking Change Warnings

Breaking changes deserve special treatment. We don't just throw an error - we provide options. I could have just left it as: `Breaking change was detected, do with this information as you wish`, I thought a little help would go a long way.

showBreakingChangeNotification(
  source: LiveSpecSource,
  changes: SpecDiff
): void {
  this.showToast({
    type: "warning",
    title: "Breaking Changes Detected",
    message: `${breakingChanges.length} breaking change${breakingChanges.length === 1 ? "" : "s"} in ${source.name}`,
    duration: 8000, // Longer duration for important warnings
    actions: [
      {
        label: "Review Changes",
        action: () => this.showChangeDiff(source, changes),
        style: "primary",
      },
      {
        label: "Skip Update",
        action: () => this.skipBreakingUpdate(source, changes),
      },
    ],
  })
}

This is like having a diplomatic advisor who warns you about potential conflicts and suggests ways to handle them.

4. Visual Change Highlighting

We built a ChangeHighlight component that shows changes directly in the collections tree. I thought that it would be a cool feature to have as a backend developer.

<template>
  <div class="change-highlight" :class="changeTypeClass">
    <div class="change-indicator" :title="changeTooltip">
      <icon :name="changeIcon" class="change-icon" />
      <span v-if="showLabel" class="change-label">{{ changeLabel }}</span>
    </div>

    <div v-if="showDetails" class="change-details">
      <div v-for="change in endpointChanges" :key="change.path + change.method" class="endpoint-change">
        <div class="endpoint-info">
          <span class="method-badge" :class="change.method.toLowerCase()">
            {{ change.method.toUpperCase() }}
          </span>
          <span class="endpoint-path">{{ change.path }}</span>
        </div>
        <div v-if="change.isBreaking" class="breaking-badge">
          <icon name="alert-triangle" />
          Breaking
        </div>
      </div>
    </div>
  </div>
</template>

This component is like having a smart highlighter that not only marks changes but also explains what they mean and why they matter.

The Magic: Notification Prioritization

const changeTypeClass = computed(() => {
  if (hasBreakingChanges.value) return "change-breaking"

  const hasModifications = endpointChanges.value.some(c => c.type === "modified")
  const hasAdditions = endpointChanges.value.some(c => c.type === "added")
  const hasRemovals = endpointChanges.value.some(c => c.type === "removed")

  if (hasRemovals) return "change-removed"
  if (hasModifications) return "change-modified"
  if (hasAdditions) return "change-added"

  return "change-none"
})

Breaking changes always take priority, followed by removals (which might break things), then modifications, then additions.


Import/Export Implementation

The Problem

Our live sync system was great, but it existed in isolation. Users had to choose between traditional import/export workflows and live sync. We needed to make live sync feel like a natural extension of existing workflows.

The Solution: Native Integration with Enhanced Options

We integrated live sync directly into the existing import/export system, making it feel like just another import option.

1. Enhanced Import/Export Component

const LiveSyncImporter: ImporterOrExporter = {
  metadata: {
    id: "live_sync",
    name: "import.connect_to_development_server",
    title: "import.live_sync_description",
    icon: IconZap, // ⚡ - because it's electric :) :) :)!
    disabled: false,
    applicableTo: ["personal-workspace", "team-workspace"],
    format: "live-sync",
  },
  component: defineStep("live_sync_import", LiveSyncImporter, () => ({
    async onSetupComplete(source) {
      // Live sync setup is complete, but we don't import collections immediately
      // Instead, we show a success message and let the sync engine handle updates
      toast.success(t("import.live_sync_connected"))

      platform.analytics?.logEvent({
        type: "HOPP_LIVE_SYNC_SETUP",
        platform: "rest",
        framework: source.framework || "unknown",
        workspaceType: isTeamWorkspace.value ? "team" : "personal",
      })
    },
  })),
}

This integration is like adding a new lane to an existing highway - it fits naturally but provides a completely different experience.

2. Connection Testing

The LiveSyncImporter component provides real-time connection testing:

async function testConnection() {
  if (!canTestConnection.value) return

  isTesting.value = true

  try {
    if (connectionType.value === "url") {
      const response = await fetch(serverUrl.value, {
        method: "GET",
        headers: { Accept: "application/json" },
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const spec = await response.json()

      // Basic OpenAPI validation
      if (!spec.openapi && !spec.swagger) {
        throw new Error(t("import.invalid_openapi_spec"))
      }

      // Generate preview collections
      const preview = await generatePreview(spec)

      connectionResult.value = {
        success: true,
        message: t("import.connection_successful"),
        preview,
      }
    }
  } catch (error) {
    connectionResult.value = {
      success: false,
      message: error instanceof Error ? error.message : t("import.connection_failed"),
    }
  } finally {
    isTesting.value = false
  }
}

This is like having a mechanic test-drive your car before you buy it - you know it works before you commit.


Team Collaboration

The Problem: Multiple Cooks in the Kitchen

Individual live sync is straightforward. Finished that in a few weeks. Team live sync is like conducting an orchestra where every musician is in a different time zone, playing from different sheet music, and some of them are improvising. Wheeew!

We needed to handle:

  • Multiple team members syncing the same source

  • Conflicts between user modifications and code changes

  • Real-time notifications across team members

  • Permission management for shared sources

The Solution

We built a team collaboration system that treats conflicts as opportunities for coordination rather than problems to avoid.

1. Team Live Sync Service

class TeamLiveSyncService {
  /**
   * Sync a team source with conflict detection
   */
  async syncTeamSource(
    sourceId: string,
    teamId: string,
    userId: string
  ): Promise<SyncResult> {
    // Check for concurrent syncs
    const concurrentSync = this.syncEvents.find(
      event => 
        event.sourceId === sourceId && 
        event.type === 'sync_started' &&
        event.timestamp > new Date(Date.now() - 30000) // Within last 30 seconds
    )

    if (concurrentSync && concurrentSync.userId !== userId) {
      // Handle concurrent sync conflict
      await this.handleConcurrentSyncConflict(sourceId, teamId, userId, concurrentSync.userId)
      throw new Error('Concurrent sync detected. Please resolve conflict.')
    }

    // Perform the sync with team coordination
    const result = await syncEngineService.triggerSync(sourceId)

    // Notify team members if there were changes
    if (result.success && result.changes) {
      this.notifyTeamMembers(teamId, sourceId, result.changes, userId)
    }

    return result
  }
}

This service is like having a project manager who prevents team members from stepping on each other's toes while keeping everyone informed.

three blue and white stick figures are fighting with each other

2. Conflict Resolution

When conflicts arise, we don't just throw an error - we provide structured resolution options:

async handleUserVsCodeConflict(
  sourceId: string,
  teamId: string,
  userChanges: any,
  codeChanges: SpecDiff
): Promise<void> {
  const conflict: ConflictResolution = {
    conflictId: `conflict_${Date.now()}`,
    sourceId,
    teamId,
    conflictType: 'user_vs_code_changes',
    description: 'Team member modifications conflict with code changes',
    options: [
      {
        id: 'keep_user_changes',
        label: 'Keep Team Changes',
        description: 'Preserve team member modifications and skip code updates',
        action: async () => {
          // Skip the code update
          await this.resolveConflict(conflict.conflictId, 'keep_user_changes')
        },
      },
      {
        id: 'apply_code_changes',
        label: 'Apply Code Changes',
        description: 'Apply code changes and overwrite team modifications',
        action: async () => {
          await syncEngineService.applyChanges(sourceId, codeChanges)
          await this.resolveConflict(conflict.conflictId, 'apply_code_changes')
        },
      },
      {
        id: 'merge_changes',
        label: 'Merge Changes',
        description: 'Attempt to merge both sets of changes',
        action: async () => {
          await this.mergeChanges(sourceId, userChanges, codeChanges)
          await this.resolveConflict(conflict.conflictId, 'merge_changes')
        },
      },
    ],
  }

  this.activeConflicts.push(conflict)
}

Ever encountered git merge conflicts? Yas yas! That’s what we are talking about. But I’m no Linus Torvalds.


Framework Optimization

The Problem

Different frameworks have different quirks, different default endpoints, different error messages, and different optimization needs. FastAPI isn't Express; it also isn't Spring Boot. We needed a system that could adapt to each framework's personality. I foresee having to update this code in the future when more frameworks are shipped. However, are we not at the point where we don’t need new frameworks?

maybe we won 't need another one written on a picture

The Solution

We built a framework optimization system that provides tailored experiences for each supported framework.

1. Framework Configuration System

class FrameworkOptimizationService {
  private initializeFrameworkConfigs(): void {
    // FastAPI Configuration
    this.frameworkConfigs.set('fastapi', {
      name: 'fastapi',
      displayName: 'FastAPI',
      defaultEndpoints: ['/openapi.json', '/docs/openapi.json'],
      commonPorts: [8000, 8080, 3000],
      setupGuide: `
# FastAPI Live Sync Setup

1. Ensure your FastAPI app is running in development mode
2. The OpenAPI spec is typically available at \`/openapi.json\`
3. Make sure CORS is configured for local development:

\`\`\`python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Hoppscotch URL
    allow_methods=["GET"],
    allow_headers=["*"],
)
\`\`\`
      `,
      errorMessages: {
        connection_failed: 'FastAPI server not running. Start with: uvicorn main:app --reload',
        cors_error: 'CORS not configured. Add CORSMiddleware to your FastAPI app',
        spec_not_found: 'OpenAPI spec not found. Check if /openapi.json is accessible',
      },
      optimizations: {
        debounceMs: 1000, // FastAPI reload can be slow
        batchUpdates: true,
        selectiveSync: true,
      },
    })
  }
}

Each framework gets its own personality profile with specific guidance, error messages, and optimization settings.

2. Error Recovery

When things go wrong, the system tries to fix them automatically…because we don’t want the developer to have to worry about it, until we can’t do anything anymore, and the developer has to get their hands dirty.

private async recoverFastAPI(source: LiveSpecSource, error: string): Promise<{ recovered: boolean; message: string }> {
  if (error.includes('Connection refused')) {
    // Try common FastAPI ports
    const commonPorts = [8000, 8080, 3000]
    for (const port of commonPorts) {
      const testUrl = source.url?.replace(/:\d+/, `:${port}`)
      if (testUrl && testUrl !== source.url) {
        try {
          const response = await fetch(testUrl, { method: 'HEAD' })
          if (response.ok) {
            return {
              recovered: true,
              message: `Found FastAPI server on port ${port}. Update your source URL.`,
            }
          }
        } catch {
          // Continue trying other ports
        }
      }
    }
  }

  return { recovered: false, message: 'FastAPI server not found on common ports. Start with: uvicorn main:app --reload' }
}

3. Webhook Integration for CI/CD

We added webhook support for seamless CI/CD integration:

async setupWebhook(
  sourceId: string,
  webhookUrl: string,
  events: string[] = ['push', 'pull_request'],
  secret?: string
): Promise<WebhookConfig> {
  const webhookConfig: WebhookConfig = {
    sourceId,
    webhookUrl,
    secret,
    events,
    isActive: true,
  }

  this.webhookConfigs.set(sourceId, webhookConfig)

  changeNotificationService.showToast({
    type: 'success',
    title: 'Webhook Configured',
    message: 'CI/CD webhook integration is now active',
  })

  return webhookConfig
}

Now your API collections can update automatically when you push code to your repository.


How It All Works Together

The Complete User Journey

  1. Developer starts coding: Framework detection identifies their stack

  2. Live sync connects: Framework-specific optimizations kick in

  3. Code changes detected: Smart notifications appear with context

  4. Team member joins: Collaboration features prevent conflicts

  5. CI/CD pipeline runs: Webhook triggers automatic sync

  6. Breaking change detected: Warnings with resolution options

// When a sync completes, multiple systems coordinate:

// 1. Sync engine detects changes
const syncResult = await syncEngineService.triggerSync(sourceId)

// 2. Framework optimization applies filtering
const filteredChanges = frameworkOptimizationService.applySelectiveSync(sourceId, syncResult.changes)

// 3. Team service handles collaboration
if (isTeamSource) {
  await teamLiveSyncService.handleTeamSync(sourceId, teamId, userId, filteredChanges)
}

// 4. Notification service provides feedback
changeNotificationService.showSyncToast(source, filteredChanges, syncResult.success)

// 5. UI components update reactively
// All status indicators, change highlights, and team activity feeds update automatically

This coordination happens automatically, creating a seamless experience where each system enhances the others.

Before: The Old Way

  • Manual imports when APIs change

  • No visibility into what changed

  • Team members overwriting each other's work

  • Generic error messages that don't help

  • One-size-fits-all approach for all frameworks

After: The New Way

  • Automatic updates with intelligent notifications

  • Rich diff views showing exactly what changed

  • Team collaboration with conflict resolution

  • Framework-specific guidance and error recovery

  • Selective sync for large APIs

  • Webhook integration for CI/CD workflows


Ready to experience the future of collaborative API development? Check out the Hoppscotch repository.

Next up: We'll dive into performance optimizations, advanced error handling, and platform integrations that make live sync work seamlessly across different development environments. This should be the final step in this integration, and you’ll be able to use it. Stay tuned!