Skip to content

Exchanges

Customize how GraphQL operations are executed with composable middleware.

What are Exchanges?

Exchanges are composable middleware that process your GraphQL operations. Each exchange can modify requests, handle responses, or add features like authentication, caching, and logging.

Acknowledgment

Mearie's exchange architecture is based on urql's exchange system, which provides a proven pattern for composable GraphQL middleware.

Basic Usage

typescript
import { createClient, httpExchange, cacheExchange } from '@mearie/react'; // or @mearie/vue, @mearie/svelte, @mearie/solid
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [cacheExchange(), httpExchange({ url: 'https://api.example.com/graphql' })],
});

How Exchanges Work

Exchanges use a stream-based architecture. Operations flow as a stream through each exchange, and results flow back:

Operations Stream

[dedupExchange]   ← Filters duplicate operations

[cacheExchange]   ← Returns cached data or forwards

[httpExchange]    ← Executes network requests

Results Stream

Each exchange receives a stream of operations, transforms it, and produces a stream of results. This enables:

  • Reactive - Operations automatically flow through the pipeline
  • Composable - Exchanges operate independently and compose cleanly
  • Cancellable - Unsubscribing cancels in-flight operations

Learn More

For a conceptual understanding of streams, see Streams Concept. For the complete API, see Streams Reference.

Terminating vs Non-terminating Exchanges

Exchanges fall into two categories based on their role in the stream pipeline:

Non-terminating exchanges transform the operation stream and forward it:

Terminating exchanges handle operations and don't forward them further:

WARNING

You must include at least one terminating exchange (usually httpExchange) at the end of your chain. Without it, your operations won't execute.

Execution Order

The order you define exchanges determines the execution flow:

typescript
export const client = createClient({
  schema,
  exchanges: [
    dedupExchange(), // 1. Deduplication (outermost)
    retryExchange(), // 2. Retry logic
    cacheExchange(), // 3. Cache layer
    httpExchange(), // 4. Network request (innermost)
  ],
});

This means:

  • Deduplication filters duplicate operations first
  • Retries will trigger the cache + HTTP flow (downstream only)
  • Cache can return early without hitting HTTP
  • HTTP terminates the chain by making the actual request

Built-in Exchanges

HTTP Exchange

Sends GraphQL requests over HTTP (terminating exchange):

typescript
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [httpExchange({ url: 'https://api.example.com/graphql' })],
});

Learn more about HTTP Exchange →

Cache Exchange

Normalized caching with automatic updates:

typescript
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [cacheExchange(), httpExchange({ url: 'https://api.example.com/graphql' })],
});

Learn more about Cache Exchange →

Retry Exchange

Automatic retry with exponential backoff:

typescript
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [retryExchange({ maxAttempts: 3 }), httpExchange({ url: 'https://api.example.com/graphql' })],
});

Learn more about Retry Exchange →

Deduplication Exchange

Deduplicates identical concurrent requests:

typescript
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [dedupExchange(), cacheExchange(), httpExchange({ url: 'https://api.example.com/graphql' })],
});

Learn more about Deduplication Exchange →

Subscription Exchange

Handles real-time subscriptions via Server-Sent Events or WebSocket (terminating exchange):

typescript
import { createClient as createSSEClient } from 'graphql-sse';
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [
    httpExchange({ url: 'https://api.example.com/graphql' }),
    subscriptionExchange({
      client: createSSEClient({
        url: 'https://api.example.com/graphql',
      }),
    }),
  ],
});

Learn more about Subscription Exchange →

Common Patterns

typescript
import { createClient as createSSEClient } from 'graphql-sse';
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [
    dedupExchange(),
    retryExchange({ maxAttempts: 3 }),
    cacheExchange(),
    httpExchange({ url: 'https://api.example.com/graphql' }),
    subscriptionExchange({
      client: createSSEClient({
        url: 'https://api.example.com/graphql',
      }),
    }),
  ],
});

Retry on Network Errors

typescript
import { schema } from '$mearie';

export const client = createClient({
  schema,
  exchanges: [
    retryExchange({
      maxAttempts: 3,
      retryIf: (error) => error.networkError,
    }),
    httpExchange({ url: 'https://api.example.com/graphql' }),
  ],
});

Creating Custom Exchanges

Exchanges transform operation streams. Here's a simple logging exchange:

typescript
import { pipe, tap } from '@mearie/core/stream';
import { type Exchange } from '@mearie/react';
import { schema } from '$mearie';

const loggingExchange = (): Exchange => {
  return ({ forward }) => {
    return (ops$) => {
      return pipe(
        ops$,
        tap((op) => console.log('Operation:', op)),
        forward,
        tap((result) => console.log('Result:', result)),
      );
    };
  };
};

export const client = createClient({
  schema,
  exchanges: [loggingExchange(), httpExchange({ url: 'https://api.example.com/graphql' })],
});

The exchange:

  1. Receives operations stream ops$
  2. Logs each operation with tap
  3. Forwards to next exchange with forward
  4. Logs each result with tap
  5. Returns the transformed results stream

Learn more about Custom Exchanges →

Exchange Interface

typescript
type Exchange = (input: ExchangeInput) => ExchangeIO;

type ExchangeInput = {
  forward: ExchangeIO;
  client: Client;
};

type ExchangeIO = (operations: Source<Operation>) => Source<OperationResult>;

An exchange:

  • Takes forward (next exchange) and client as input
  • Returns a function that transforms operation streams to result streams
  • Uses stream operators to transform operations and results

Best Practices

  • Order exchanges from outermost (retry, dedup) to innermost (HTTP)
  • Always end the chain with a terminating exchange (e.g., httpExchange)
  • Keep custom exchanges simple and focused on a single concern

Next Steps