How Macro's GraphQL APIs Expose Data to the Frontend with Proper Caching
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. The router() function attaches the GraphiQL IDE, the standard GraphQL handler, and the WebSocket handler for subscriptions.
// 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.
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 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.
// 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. 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. 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 fetches recently viewed soup items with a five-minute stale time.
// 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 demonstrates patching cached query data without triggering a full refetch.
// 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
SoupItemDataLoaderand related loaders viainsert_graphql_context_datainservices/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 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.
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 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. 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, ensuring consistent validation and zero regeneration overhead per request.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →