# How Macro's GraphQL APIs Expose Data to the Frontend with Proper Caching

> Learn how Macro's GraphQL APIs expose data to the frontend with efficient caching. Discover how request-scoped DataLoaders and Solid-Query/URQL caching eliminate redundant requests.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: how-to-guide
- Published: 2026-08-18

---

**Macro's Rust backend serves GraphQL data through request-scoped Async-GraphQL DataLoaders, while the Solid frontend caches responses via Solid-Query and URQL, eliminating redundant database hits and network requests.**

Macro's open-source repository (`macro-inc/macro`) powers its document platform with a Rust-based GraphQL layer that serves a Solid.js web UI. The architecture relies on two distinct caching strategies—server-side request memoization and client-side query persistence—to ensure the frontend receives consistent, low-latency data without redundant backend work.

## Server-Side Caching with Request-Scoped DataLoaders

The Rust backend uses **Async-GraphQL** endpoints mounted at `/soup/graphql` for queries and mutations and `/soup/graphql/ws` for subscriptions. Each incoming request receives its own set of DataLoaders through the GraphQL context, which batch identical key lookups and cache the results for the lifetime of that single request.

### GraphQL Router Registration

The HTTP router exposes the GraphQL handlers in [`services/document_storage_service/src/api/graphql_soup.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/graphql_soup.rs). The `router()` function attaches the GraphiQL IDE, the standard GraphQL handler, and the WebSocket handler for subscriptions.

```rust
// services/document_storage_service/src/api/graphql_soup.rs
pub(crate) fn router() -> Router<ApiContext> {
    Router::new()
        .route(GRAPHQL_PATH, get(graphiql).post(graphql_handler))
        .route(GRAPHQL_SUBSCRIPTION_PATH, get(graphql_ws_handler))
}

```

This centralizes all GraphQL traffic under a single service boundary, allowing every request to share the same pre-built schema while still maintaining isolated loader state.

### Request Context and DataLoader Injection

For every request, `graphql_query_context_data` constructs a fresh `async_graphql::Request` and populates it with the shared schema, mutation service, notification reader, and the DataLoaders. The helper `insert_graphql_context_data` inserts `state.soup_item_loader.clone()` into the request data map.

```rust
fn graphql_query_context_data(state: &ApiContext, mut req: async_graphql::Request) -> async_graphql::Request {
    insert_graphql_context_data(&mut req.data, state, macro_user_id, organization_id);
    req
}

fn insert_graphql_context_data(
    data: &mut async_graphql::Data,
    state: &ApiContext,
    macro_user_id: Option<Uuid>,
    organization_id: Option<Uuid>,
) {
    // … other inserts …
    data.insert(state.graphql_entity_mutation_service.clone());
    data.insert(state.graphql_notification_reader.clone());
    // DataLoader for Soup items
    data.insert(state.soup_item_loader.clone());
}

```

By cloning the loader into each request's context, Macro ensures that repeated entity lookups inside a single query are fetched once and then served from memory for the remainder of that request.

### Resolver Batching with SoupItemDataLoader

Resolvers in [`crates/graphql_soup/src/resolvers.rs`](https://github.com/macro-inc/macro/blob/main/crates/graphql_soup/src/resolvers.rs) retrieve the cached loader via `ctx.data::<SoupItemDataLoader>()?`. When a resolver calls `load_many`, Async-GraphQL coalesces all identical keys that appear within the same execution tick into a single async batch fetch.

```rust
// crates/graphql_soup/src/resolvers.rs
async fn some_resolver(ctx: &Context<'_>, ids: Vec<Uuid>) -> async_graphql::Result<Vec<SoupItem>> {
    let loader = ctx.data::<SoupItemDataLoader>()?;
    let items = loader.load_many(ids).await?;
    Ok(items)
}

```

The `SoupItemDataLoader` implementation lives in [`crates/graphql_soup/src/loaders.rs`](https://github.com/macro-inc/macro/blob/main/crates/graphql_soup/src/loaders.rs). It groups redundant keys and performs a single database or service call, storing the returned entities in a request-local hash map. This pattern prevents N+1 queries when a GraphQL selection resolves nested collections.

### Schema Generation and Reuse

The complete schema is built once at startup using `complete_graph::build_schema_from_arcs` in [`services/document_storage_service/src/main.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/main.rs). The resulting `graphql_soup_schema` is reused for every request, ensuring consistent type definitions and validation rules without regeneration overhead.

## Client-Side Caching with Solid-Query and URQL

The `apps/web` frontend relies on a thin URQL wrapper integrated with Solid.js. **Solid-Query** (`@tanstack/solid-query`) sits above this layer to cache query results, deduplicate in-flight requests, and manage background revalidation.

### URQL-Solid Client Integration

A lightweight URQL client is created via `createUrqlClient` and placed into Solid's context. The utilities in `apps/web/src/lib/urql-solid/` expose a `useQuery`-compatible API that bridges URQL's network layer with Solid-Query's cache.

### Query Hooks and Cache Keys

UI components call generated query functions backed by Solid-Query's `useQuery` hook. The hook caches responses in a client-side store keyed by the query string and variables. The following example from [`apps/web/src/lib/queries/soup/recently-viewed.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/soup/recently-viewed.ts) fetches recently viewed soup items with a five-minute stale time.

```ts
// apps/web/src/lib/queries/soup/recently-viewed.ts
import { useQuery } from '@tanstack/solid-query';
import { graphql } from 'src/lib/urql-solid/create-urql-query';

export const useRecentlyViewed = () => {
  return useQuery(() => ({
    queryKey: ['recentlyViewed'],
    queryFn: async () => {
      const res = await graphql(`
        query RecentlyViewed {
          recentlyViewed {
            id
            title
            updatedAt
          }
        }
      `);
      return res.recentlyViewed;
    },
    staleTime: 5 * 60_000, // 5 min
  }));
};

```

Solid-Query automatically deduplicates concurrent identical requests, serves cached data on navigation, and refetches when the window regains focus or when a mutation invalidates related query keys. This eliminates redundant network round-trips across the Macro frontend.

### Subscription-Driven Cache Updates

Real-time updates arrive through the `/soup/graphql/ws` WebSocket endpoint. The client subscribes using URQL-Solid subscription utilities, and incoming patches are merged directly into the Solid-Query cache. The helper in [`apps/web/src/lib/queries/soup/backfill.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/soup/backfill.ts) demonstrates patching cached query data without triggering a full refetch.

```ts
// apps/web/src/lib/queries/soup/backfill.ts
import { createSubscription } from 'src/lib/urql-solid/create-urql-query';
import { useQueryClient } from 'src/lib/queries/client';

export const subscribeSoupPatches = () => {
  const queryClient = useQueryClient();

  createSubscription(`
    subscription SoupPatch {
      soupPatch {
        entityId
        patch
      }
    }
  `, {
    onData: ({ soupPatch }) => {
      // Merge patch into the cached query data
      queryClient.setQueryData(['recentlyViewed'], old => {
        // …apply patch logic…
        return old;
      });
    },
  });
};

```

Because the patch mutates the existing cache entry, Solid's reactivity system updates only the affected UI components instantly.

## Summary

- **Request-scoped DataLoaders** in the Rust backend batch and memoize entity fetches for the lifetime of a single GraphQL request, preventing N+1 queries.
- The **Async-GraphQL context** injects `SoupItemDataLoader` and related loaders via `insert_graphql_context_data` in [`services/document_storage_service/src/api/graphql_soup.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/graphql_soup.rs).
- The **Solid-Query** frontend cache stores GraphQL responses by query key and variables, deduplicating concurrent requests and serving stale data while revalidating in the background.
- **GraphQL WebSocket subscriptions** push real-time patches into the Solid-Query cache through URQL-Solid, keeping the UI synchronized without full refetches.

## Frequently Asked Questions

### How does Macro prevent duplicate database queries within a single GraphQL request?

Macro uses Async-GraphQL DataLoaders scoped to each individual request. When resolvers in [`crates/graphql_soup/src/resolvers.rs`](https://github.com/macro-inc/macro/blob/main/crates/graphql_soup/src/resolvers.rs) call `loader.load_many()`, the `SoupItemDataLoader` groups identical keys that occur in the same execution tick and issues a single batch fetch. The results are memoized in the request context, so subsequent resolver calls for the same keys return cached values instantly.

### What client-side library handles caching in Macro's web frontend?

The frontend relies on **Solid-Query** (`@tanstack/solid-query`) layered over a thin URQL client. Solid-Query caches query results by key, deduplicates in-flight requests, and handles background revalidation. This setup is defined in query modules such as [`apps/web/src/lib/queries/soup/recently-viewed.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/soup/recently-viewed.ts).

### How do real-time GraphQL subscriptions update the frontend cache?

Subscriptions connect to `/soup/graphql/ws` via URQL-Solid utilities. When a `SoupPatch` payload arrives, the subscription callback in [`apps/web/src/lib/queries/soup/backfill.ts`](https://github.com/macro-inc/macro/blob/main/apps/web/src/lib/queries/soup/backfill.ts) calls `queryClient.setQueryData()` to merge the patch into the existing cached dataset. Solid's fine-grained reactivity then updates only the affected DOM nodes.

### Where is the GraphQL schema generated and reused across requests?

The schema is built once at startup by `complete_graph::build_schema_from_arcs` inside [`services/document_storage_service/src/main.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/main.rs). The resulting schema object is stored in application state and reused for every request handled by the router in [`services/document_storage_service/src/api/graphql_soup.rs`](https://github.com/macro-inc/macro/blob/main/services/document_storage_service/src/api/graphql_soup.rs), ensuring consistent validation and zero regeneration overhead per request.