grafilter
A Rust library for building GraphQL proxies with field-level interception. Sits between your client and an upstream GraphQL server, letting you intercept, transform, or fully resolve individual fields — all through a single unified API.
Why?
Existing GraphQL middleware libraries either wrap local resolvers (not proxies) or operate at the schema level (rename types, filter fields). None of them give you a single closure where you decide at runtime whether to call upstream, resolve locally, or transform the response.
grafilter fills that gap. One intercept call, one closure, full control:
- Resolve locally — return a value without ever hitting upstream
- Forward and transform — call upstream, then modify the result
- Conditional forwarding — check a cache first, only call upstream on miss
- Access parent fields — read sibling fields from the parent object
- Per-request state — inject request-scoped data (tenant ID, auth, etc.) accessible in every interceptor
Quick start
use ;
use ;
use Arc;
// You provide the executor — any async function that sends a query
// string to your upstream GraphQL server and returns the JSON response.
let executor: Executor = new;
let proxy = builder
.intercept
.build;
let result = proxy
.execute
.await?;
Schema-aware type matching
Without a schema, interceptors match by parent field name: ("user", "email") intercepts email under the user field. This works but breaks if the same type appears under different field names (e.g., author is also a User).
With a schema, interceptors match by actual GraphQL type name:
// Fetch the introspection result from your upstream server
let schema_json = my_client
.post
.await?;
let proxy = builder
.with_schema?
// Matches email on ANY User, whether it's under "user", "author",
// "assignee", etc.
.intercept
.build;
Per-request state
Similar to Axum's .with_state(), you can pass request-scoped state that every interceptor can access:
let proxy = builder
.intercept
.build;
// State is passed per-request
let ctx = RequestCtx ;
let result = proxy
.execute
.await?;
Interceptor patterns
Resolve locally (skip upstream entirely)
.intercept
Forward and transform
.intercept
Cache with conditional forwarding
.intercept
Use parent fields
.intercept
InterceptContext API
Inside an interceptor closure, ctx provides:
| Method | Description |
|---|---|
ctx.forward() |
Call upstream for this field and return the result |
ctx.parent() |
The parent object's fields (from upstream response) |
ctx.state() |
Per-request state passed to execute() |
ctx.field_name() |
Name of the intercepted field |
ctx.args() |
Arguments passed to this field in the query |
Roadmap
- Batched forwarding — collect all
forward()calls and batch into a single upstream request instead of individual calls per field - Wildcard selectors —
("*", "email")to intercept a field on any type forward_with— modify the sub-selection before forwarding to upstream
License
MIT