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.
Install
Section titled “Install”cargo add nest-rs --features graphql,seaorm,authzOne 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:
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.
The minimal resolver
Section titled “The minimal resolver”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:
use nest_rs::core::module;use nest_rs::graphql::GraphqlModule;use features::greeting::GreetingModule;
#[module(imports = [GreetingModule, GraphqlModule::for_root(None)])]pub struct GreetingsAppModule;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.
Run it
Section titled “Run it”$ 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.
Injecting a service
Section titled “Injecting a service”A resolver injects providers like any other struct:
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.
In this section
Section titled “In this section”Basics
Section titled “Basics”- Queries and mutations —
#[query],#[mutation], inputs, outputs, returning typed entities. - Field resolvers — custom computed
fields with
#[field_resolver], and the oneComplexObjectcaveat to watch for. - Relations resolve themselves — declare a SeaORM relation, get a typed GraphQL field with a batched loader and cursor pagination for free.
- Errors —
async_graphql::Error, extensions, and mappingServiceErrorto a GraphQL envelope.
All options
Section titled “All options”- Configuration —
GraphqlConfig, 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.
Reference
Section titled “Reference”crates/features/src/users/graphql/resolver.rs— a production-grade resolver with relations, dataloaders and authorization.posts/graphql/resolver.rsin the demo — thepublishPostmutation 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.
Going further
Section titled “Going further”- 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.