Skip to content

GraphQL

A self-composing schema, resolvers as structs, queries and mutations on one impl block.

Write a resolver as a struct and decorate it with #[resolver]; its operations register themselves into the schema at boot — there is no central queries = [...] list to keep in sync. Root resolvers (#[query], #[mutation]) and entity-field resolvers (#[field_resolver]) live on the same impl block. The schema composes itself from a link-time registry, the playground self-mounts in dev, and the SDL is committed.

nest-rs-graphql builds on async-graphql (served through async-graphql-poem) — the framework wraps it so resolvers register themselves and inject providers like any other struct.

Terminal window
cargo add nest-rs --features graphql,seaorm,authz

One line in the feature crate’s manifest. graphql alone runs a resolver over a hand-written service; add seaorm when the resolver binds an entity, authz when an operation carries #[authorize] — features you enable because your code says so.

#[operations] builds on async-graphql’s own #[Object], and a third-party macro expands against your prelude — so the decorator pins that expansion to the framework’s re-export (crate = "::nest_rs::graphql::async_graphql"). Reach async-graphql’s types the same way and the line above stays the only one:

crates/features/src/greeting/graphql/resolver.rs
use nest_rs::graphql::async_graphql::{Context, Result};

Declare async-graphql = "7" yourself only when you write its derives by hand (#[derive(SimpleObject)] on a type of your own) — a derive you wrote expands against your prelude, exactly like serde’s.

crates/features/src/greeting/graphql/resolver.rs
use nest_rs::graphql::{operations, resolver};
#[resolver]
pub struct GreetingResolver;
#[operations]
impl GreetingResolver {
#[query]
#[public]
async fn greeting(&self, name: Option<String>) -> String {
format!("Hello, {}!", name.as_deref().unwrap_or("World"))
}
}
  • #[resolver] on the struct registers it with the schema discovery registry.
  • #[operations] on the impl block orchestrates the operations within — one decorator per item shape, exactly like #[controller] / #[routes].
  • #[query] declares a top-level query. The method name becomes the field name (camelCased on the wire: greeting).
  • #[public] declares the operation’s access posture — deliberately ungated. Every #[query] / #[mutation] carries either #[public] or #[authorize(Action, Entity)] (ability gate + automatic response masking); a method with neither does not compile.
  • Arguments are mapped to GraphQL input arguments by serde. Option<T> becomes nullable.

#[query], #[mutation], #[field_resolver], #[public], #[authorize] and #[inject] are inner attributes the two macros consume in place — there is nothing to import for them. Only resolver and operations (and crud / dataloader on the pages that use them) come from nest_rs::graphql.

Mount the GraphQL transport once at the app root:

apps/api/src/module.rs
use nest_rs::core::module;
use nest_rs::graphql::GraphqlModule;
use features::greeting::GreetingModule;
#[module(imports = [GreetingModule, GraphqlModule::for_root(None)])]
pub struct GreetingsAppModule;
crates/features/src/greeting/module.rs
use nest_rs::core::module;
use super::resolver::GreetingResolver;
#[module(providers = [GreetingResolver])]
pub struct GreetingModule;

GraphqlModule::for_root mounts POST /graphql (the endpoint) and, when the playground is on, GET /graphql for the interactive client.

Terminal window
$ curl -sX POST http://localhost:3000/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ greeting(name: \"Ada\") }"}'
{"data":{"greeting":"Hello, Ada!"}}

In dev (NESTRS_GRAPHQL__PLAYGROUND=true), open http://localhost:3000/graphql for the playground.

A resolver injects providers like any other struct:

crates/features/src/greeting/graphql/resolver.rs
use std::sync::Arc;
use nest_rs::graphql::{operations, resolver};
use crate::greeting::service::GreetingService;
#[resolver]
pub struct GreetingResolver {
#[inject]
svc: Arc<GreetingService>,
}
#[operations]
impl GreetingResolver {
#[query]
#[public]
async fn greeting(&self, name: Option<String>) -> String {
self.svc.greet(name.as_deref().unwrap_or("World"))
}
}

The container resolves GreetingService from the import tree. No container handle to thread, no factory to call.

  • Queries and mutations#[query], #[mutation], inputs, outputs, returning typed entities.
  • Field resolvers — custom computed fields with #[field_resolver], and the one ComplexObject caveat to watch for.
  • Relations resolve themselves — declare a SeaORM relation, get a typed GraphQL field with a batched loader and cursor pagination for free.
  • Errorsasync_graphql::Error, extensions, and mapping ServiceError to a GraphQL envelope.
  • ConfigurationGraphqlConfig, SDL emission, playground toggle, mount path.
  • Subscriptions#[subscription] over graphql-ws, gated at subscribe and filtered per item pushed.
  • Query limits — depth and complexity ceilings, and what a rejected query looks like.
  • crates/features/src/users/graphql/resolver.rs — a production-grade resolver with relations, dataloaders and authorization.
  • posts/graphql/resolver.rs in the demo — the publishPost mutation and draft/published status.
  • crates/nest-rs-graphql/#[resolver], #[query], #[mutation], #[field_resolver], schema composition.
  • crates/nest-rs-resource/#[expose] for an entity that becomes a GraphQL type and an OpenAPI schema from one declaration.
  • Security — bind the ability guard so resolvers run with an authenticated principal and an ambient Ability.
  • Database — the data layer the relation pages build on.
  • Dataloaders#[dataloader] on a service method, for batched fetches beyond what relations give you.
  • WebSockets — the other realtime surface, for protocols that are message-shaped rather than schema-shaped.