On this page

🔗The Multi-Protocol Challenge

Modern applications don't use a single protocol. They use REST APIs, GraphQL queries, WebSocket connections, gRPC services, and raw TCP. Each protocol has different semantics, different connection models, and different mocking requirements.

The challenge isn't just supporting multiple protocols—it's supporting them in a unified way that doesn't duplicate logic. You don't want separate code paths for HTTP mocking, GraphQL mocking, and WebSocket mocking. You want a unified architecture where protocol-specific handlers share common infrastructure.

MockForge v0.1.3 introduced GraphQL and WebSocket support, building on the existing HTTP and gRPC infrastructure. This post covers the technical implementation of how protocol abstraction enables multi-protocol mocking without code duplication.

🔗Protocol Abstraction: The Core Architecture

The protocol abstraction layer is the key to supporting multiple protocols:

graph TD
    A[Protocol Abstraction] -->|HTTP| B[HTTP Handler]
    A -->|GraphQL| C[GraphQL Handler]
    A -->|WebSocket| D[WebSocket Handler]
    A -->|gRPC| E[gRPC Handler]
    A -->|TCP| F[TCP Handler]
    
    G[Data Generation] -->|Schema| A
    H[Response Selection] -->|Examples| A
    I[State Management] -->|Shared State| A

The abstraction layer defines a unified interface that all protocols implement. This means data generation, response selection, and state management work the same way regardless of protocol.

🔗GraphQL Implementation: Schema-Based Mocking

GraphQL mocking is fundamentally different from REST. Instead of matching URLs and HTTP methods, you match GraphQL queries and mutations. The implementation is built around GraphQL schemas:

pub struct GraphQLHandler {
    schema: GraphQLSchema,
    resolvers: ResolverRegistry,
    executor: GraphQLExecutor,
}

impl GraphQLHandler {
    pub async fn handle_query(&self, query: &str, variables: &Value) -> Result<Value> {
        // Parse and validate query
        let document = parse_query(query)?;
        validate_query(&document, &self.schema)?;

        // Execute query with mock resolvers
        let result = self.executor.execute(
            &document,
            &variables,
            &self.schema,
            &self.resolvers,
        ).await?;

        Ok(result)
    }
}

The GraphQL handler:

  1. Parses the incoming query
  2. Validates it against the schema
  3. Executes it using mock resolvers
  4. Returns the result

🔗Automatic Resolver Generation

GraphQL resolvers are generated automatically from the schema. The generator analyzes field types and generates appropriate mock data:

pub struct ResolverGenerator;

impl ResolverGenerator {
    fn generate_resolver(field: &Field, schema: &GraphQLSchema) -> Resolver {
        match field.type_.as_str() {
            "ID" => Resolver::scalar(|_| generate_uuid()),
            "String" => Resolver::scalar(|_| generate_string()),
            "Int" => Resolver::scalar(|_| generate_int()),
            "Boolean" => Resolver::scalar(|_| generate_bool()),
            "User" => Resolver::object(|_| {
                generate_user_object(schema)
            }),
            "[User]" => Resolver::list(|_| {
                (0..10).map(|_| generate_user_object(schema)).collect()
            }),
            _ => Resolver::default(),
        }
    }
}

The resolver generator:

  • Analyzes field types from the schema
  • Generates appropriate mock data for each type
  • Maintains referential integrity (e.g., user.posts references the same user)
  • Supports nested queries and relationships

🔗GraphQL Query Execution

The query executor processes GraphQL queries and resolves fields:

pub struct GraphQLExecutor {
    schema: GraphQLSchema,
    resolvers: ResolverRegistry,
}

impl GraphQLExecutor {
    pub async fn execute(
        &self,
        query: &Document,
        variables: &Value,
        root: &Value,
    ) -> Result<Value> {
        // Extract operation (query or mutation)
        let operation = query.operation()?;

        // Execute selection set
        let result = self.execute_selection_set(
            &operation.selection_set,
            &root,
            variables,
        ).await?;

        Ok(result)
    }

    async fn execute_selection_set(
        &self,
        selection_set: &SelectionSet,
        parent: &Value,
        variables: &Value,
    ) -> Result<Value> {
        let mut result = json!({});

        for selection in &selection_set.selections {
            match selection {
                Selection::Field(field) => {
                    let field_name = &field.name;
                    let resolver = self.resolvers.get(field_name)?;

                    // Resolve field value
                    let value = resolver.resolve(parent, variables).await?;

                    // Handle nested selections
                    if let Some(nested_selection) = &field.selection_set {
                        let nested_value = self.execute_selection_set(
                            nested_selection,
                            &value,
                            variables,
                        ).await?;
                        result[field_name] = nested_value;
                    } else {
                        result[field_name] = value;
                    }
                }
                Selection::FragmentSpread(_) => {
                    // Handle fragment spreads
                }
                Selection::InlineFragment(_) => {
                    // Handle inline fragments
                }
            }
        }

        Ok(result)
    }
}

The executor handles:

  • Operation parsing (queries, mutations)
  • Field resolution with mock resolvers
  • Nested selections (relationships)
  • Fragment spreads and inline fragments
  • Variable substitution

🔗WebSocket Implementation: Connection Management

WebSocket mocking is different from HTTP. It's not request/response—it's a persistent connection with bidirectional message flow. The implementation handles connection lifecycle and message routing:

pub struct WebSocketHandler {
    connections: Arc<DashMap<String, WebSocketConnection>>,
    message_handlers: HandlerRegistry,
}

impl WebSocketHandler {
    pub async fn handle_connection(
        &self,
        stream: TcpStream,
        addr: SocketAddr,
    ) -> Result<()> {
        // Upgrade HTTP connection to WebSocket
        let ws_stream = accept_async(stream).await?;
        let (tx, rx) = ws_stream.split();

        let connection_id = generate_connection_id();
        let connection = WebSocketConnection {
            id: connection_id.clone(),
            addr,
            tx,
            rx,
            state: Arc::new(RwLock::new(ConnectionState::default())),
        };

        // Store connection
        self.connections.insert(connection_id.clone(), connection.clone());

        // Handle messages
        self.handle_messages(connection).await?;

        Ok(())
    }

    async fn handle_messages(&self, connection: WebSocketConnection) -> Result<()> {
        while let Some(msg) = connection.rx.next().await {
            match msg {
                Ok(Message::Text(text)) => {
                    // Parse message
                    let message: WebSocketMessage = serde_json::from_str(&text)?;

                    // Route to handler
                    let handler = self.message_handlers.get(&message.type_)?;
                    let response = handler.handle(&message, &connection.state).await?;

                    // Send response
                    connection.tx.send(Message::Text(
                        serde_json::to_string(&response)?
                    )).await?;
                }
                Ok(Message::Close(_)) => {
                    // Connection closed
                    break;
                }
                Err(e) => {
                    error!("WebSocket error: {}", e);
                    break;
                }
                _ => {}
            }
        }

        // Clean up connection
        self.connections.remove(&connection.id);
        Ok(())
    }
}

The WebSocket handler:

  • Manages connection lifecycle (connect, message, disconnect)
  • Routes messages to appropriate handlers
  • Maintains connection state
  • Handles connection cleanup

🔗WebSocket Message Replay

WebSocket connections can be recorded and replayed, just like HTTP requests:

pub struct WebSocketRecorder {
    database: Arc<RecorderDatabase>,
}

impl WebSocketRecorder {
    pub async fn record_message(
        &self,
        connection_id: &str,
        message: &WebSocketMessage,
        response: &WebSocketMessage,
    ) -> Result<()> {
        // Store message exchange
        self.database.record_websocket_exchange(
            connection_id,
            message,
            response,
        ).await?;

        Ok(())
    }

    pub async fn replay_connection(
        &self,
        connection_id: &str,
    ) -> Result<Vec<WebSocketMessage>> {
        // Load recorded messages for this connection
        let exchanges = self.database.get_websocket_exchanges(connection_id).await?;

        // Replay messages in order
        let messages: Vec<WebSocketMessage> = exchanges
            .into_iter()
            .map(|exchange| exchange.response)
            .collect();

        Ok(messages)
    }
}

This enables deterministic testing of WebSocket-based features by replaying recorded message sequences.

🔗Protocol Abstraction Interface

The protocol abstraction defines a unified interface:

pub trait ProtocolHandler {
    type Request;
    type Response;

    async fn handle(&self, request: Self::Request) -> Result<Self::Response>;

    fn get_schema(&self) -> &dyn ProtocolSchema;

    fn supports_feature(&self, feature: ProtocolFeature) -> bool;
}

// HTTP implementation
impl ProtocolHandler for HttpHandler {
    type Request = http::Request<Body>;
    type Response = http::Response<Body>;

    async fn handle(&self, request: Self::Request) -> Result<Self::Response> {
        // HTTP-specific handling
    }
}

// GraphQL implementation
impl ProtocolHandler for GraphQLHandler {
    type Request = GraphQLRequest;
    type Response = GraphQLResponse;

    async fn handle(&self, request: Self::Request) -> Result<Self::Response> {
        // GraphQL-specific handling
    }
}

// WebSocket implementation
impl ProtocolHandler for WebSocketHandler {
    type Request = WebSocketMessage;
    type Response = WebSocketMessage;

    async fn handle(&self, request: Self::Request) -> Result<Self::Response> {
        // WebSocket-specific handling
    }
}

This unified interface means:

  • Data generation works across all protocols
  • Response selection works the same way
  • State management is shared
  • Observability is consistent

🔗Shared Infrastructure: Data Generation and State

Protocol-specific handlers share common infrastructure:

pub struct MockServer {
    // Shared across all protocols
    data_generator: Arc<DataGenerator>,
    state_manager: Arc<StateManager>,
    response_selector: Arc<ResponseSelector>,

    // Protocol-specific handlers
    http_handler: HttpHandler,
    graphql_handler: GraphQLHandler,
    websocket_handler: WebSocketHandler,
}

impl MockServer {
    pub async fn handle_http(&self, req: http::Request<Body>) -> http::Response<Body> {
        // Use shared data generator
        let mock_data = self.data_generator.generate(&req.schema).await?;
        // ...
    }

    pub async fn handle_graphql(&self, req: GraphQLRequest) -> GraphQLResponse {
        // Use shared data generator
        let mock_data = self.data_generator.generate(&req.schema).await?;
        // ...
    }

    pub async fn handle_websocket(&self, msg: WebSocketMessage) -> WebSocketMessage {
        // Use shared data generator
        let mock_data = self.data_generator.generate(&msg.schema).await?;
        // ...
    }
}

This shared infrastructure means:

  • LLM-powered data generation works for all protocols
  • State management is consistent across protocols
  • Response selection logic is shared
  • Observability is unified

🔗GraphQL Playground Integration

GraphQL Playground provides an interactive UI for testing GraphQL queries:

pub fn create_graphql_router() -> Router {
    Router::new()
        .route("/graphql", post(graphql_handler))
        .route("/playground", get(playground_handler))
        .route("/graphql/ws", get(graphql_subscription_handler))
}

The playground handler serves the GraphQL Playground UI, which provides:

  • Schema explorer
  • Query editor with auto-complete
  • Response viewer
  • Request history

This makes it easy to test GraphQL queries interactively without writing client code.

🔗WebSocket Templating

WebSocket messages can use templates, just like HTTP responses:

pub struct WebSocketTemplateEngine;

impl WebSocketTemplateEngine {
    fn render_template(
        &self,
        template: &str,
        context: &TemplateContext,
    ) -> Result<String> {
        // Support for template variables
        // {{timestamp}} - Current timestamp
        // {{uuid}} - Generate UUID
        // {{faker.email}} - Generate fake email
        // {{state.connection_id}} - Connection state
        // ...
    }
}

This enables dynamic WebSocket responses that can simulate realistic server behavior.

🔗Performance Considerations

Multi-protocol support doesn't mean sacrificing performance. The architecture is designed for efficiency:

  • Protocol-specific handlers: Each protocol has optimized handlers
  • Shared infrastructure: Common code is shared, not duplicated
  • Async I/O: All protocols use async/await for non-blocking I/O
  • Connection pooling: WebSocket connections are efficiently managed

The result is a mock server that can handle multiple protocols simultaneously without performance degradation.

🔗The Bottom Line

GraphQL and WebSocket support in MockForge v0.1.3 demonstrate the power of protocol abstraction. By building on a unified architecture, adding new protocols doesn't require duplicating infrastructure—it just requires implementing the protocol-specific handler interface.

The architecture enables:

  • Multi-protocol mocking in a single server
  • Shared data generation and state management
  • Consistent observability across protocols
  • Easy addition of new protocols

For teams building modern applications that use multiple protocols, this architecture means you can mock your entire stack—REST APIs, GraphQL queries, WebSocket connections, gRPC services—with a single tool. That's the value of protocol abstraction.