graphql feature mounts a GraphQL endpoint on the same Axum router as your REST controllers, and the ws feature adds WebSocket gateways with the same #[injectable] provider injection and guard/pipe/interceptor cross-cutting you use everywhere else.
GraphQL
Setup
Add thegraphql feature to your nestrs dependency and add async-graphql and nestrs-graphql:
nestrs-graphql crate is re-exported as nestrs::graphql when the feature is enabled.
Defining a schema
Define your query and mutation types usingasync-graphql attributes and build a Schema. Then pass it to enable_graphql:
GET /graphql (GraphQL Playground) and POST /graphql (query endpoint) on the same router as your REST routes. The global prefix and URI versioning settings apply to the GraphQL path the same way they apply to REST controllers.
Custom path and options
Useenable_graphql_with_path to change the mount point:
enable_graphql_with_options to control Playground availability and other HTTP-surface settings:
Mutations and resolvers
A full query planner, the Apollo Router wire protocol, and automatic type merging are outside nestrs — compose subgraphs with Apollo Router / GraphOS when you need those. For stitching subgraph SDLs behind a single Axum endpoint, nestrs ships a lightweight federation gateway; see the next section.
Federation gateway
Thegraphql-federation-gateway feature enables a lightweight Apollo Federation gateway in nestrs-graphql. It stitches subgraph SDLs behind one Axum endpoint and exposes two fields:
_service { sdl }— the merged federation SDL, so a router in front of the gateway can introspect the stitched shape.- entity resolution — dispatches each representation’s
__typenameto the subgraph-specific resolver you supply (see below).
graphql-authz, and the gateway types are re-exported under nestrs::graphql::federation: federation_router, federation_router_with_options, federation_router_with_hook, FederationConfig, SubgraphSpec, EntityResolver, and FederationError.
What it is — and isn’t
The gateway validates, emits, and routes — it does not plan queries:- Validate every
SubgraphSpec.sdlat gateway-construction time. A malformed SDL refuses withFederationError::Parse— no partially-built gateway. - Emit the merged SDL with the same federation-v2 flags async-graphql uses for individual subgraphs (
@linkdirective,_Entity/_Any/_serviceplumbing). - Route cross-subgraph entity queries by
__typenameto a resolver you supply. Concurrency, batching, and transport (HTTP vs in-process) are the caller’s choice.
Exporting a subgraph’s federation SDL
Each subgraph contributes a federation-shaped SDL. Export one from an async-graphqlSchema with SDLExportOptions::default().federation(), or from a running federation subgraph:
nestrs_graphql::export_schema_sdl(&schema) at build time instead.
@link / @key directives also works — the gateway only parses it, it does not re-derive it.
Wiring the gateway
Build aSubgraphSpec per subgraph (name + SDL + entity resolver) and pass them to federation_router:
Router into your NestApplication with use_global_layer, keeping listen and all built-in middleware:
SubgraphSpec.name matching the representation’s __typename. Two subgraphs with the same name fail at construction with FederationError::Merge (the last-writer-wins alternative would silently mask config bugs), and an empty subgraphs list fails with FederationError::NoSubgraphs.
The batched EntityResolver
EntityResolver is batched, not one-representation-at-a-time. The gateway groups all representations in one entities call by __typename and hands each group to its owning resolver as a single slice:
- Each element of
representationsis the parsed JSON object the client sent (for example{ "__typename": "User", "id": "1" }). - Return one entry per input representation, in the same order —
Ok(None)maps to JSONnullin theentitieslist. - Unknown
__typenamevalues (no matching subgraph) resolve tonull; a resolver error nulls that whole group and is logged. - The gateway re-interleaves batched results back into input order before responding.
Fn(&Context, &[&Value]) -> Result<Vec<Option<Value>>> signature implements the trait via Arc::new(closure) — no manual impl block needed. To batch at the SQL level, pair the resolver with the graphql-dataloader feature’s #[dataloader] / DataLoader (1 ms batch window, one SELECT ... WHERE id IN (...) per request).
Entity resolution with authz
federation_router_with_hook(cfg, path, hook) installs a GqlHandlerHook (pass an Arc<GqlDataContext> for row-level authz). The hook wraps schema.execute_batch, so entity resolvers run inside the hook’s scope — per-request abilities, transactions, and dataloaders installed by the hook’s prepare are visible to entity resolvers unchanged.
WebSockets
Setup
Add thews feature:
nestrs-ws is re-exported as nestrs::ws when the feature is enabled.
Wire format
WebSocket frames are JSON objects with the shape{ "event": "name", "data": <json> }. The server sends the same format back to clients. Unknown frames and error conditions are sent to the client on the special "error" event name (nestrs::ws::WS_ERROR_EVENT).
Defining a gateway
Use#[ws_gateway] on a struct and #[ws_routes] on its impl block. Individual message handlers are annotated with #[subscribe_message("event-name")]:
Emitting to a client
WsClient::emit serializes a value and sends it as a JSON frame:
WsClient::emit_json sends a pre-built serde_json::Value directly.
Guards, pipes, and interceptors
Apply cross-cutting to WebSocket handlers with thews-specific attributes:
How WS handlers resolve providers from DI
Gateways are providers themselves, and#[ws_gateway] mounts them through the app’s ProviderRegistry. Register the gateway in both lists of your module:
Controller::register code generated by #[ws_gateway] resolves the gateway with registry.get::<ChatGateway>() and mounts it with ws_route_with_registry, which wraps it so the shared runtime reaches the registry-aware dispatch generated by #[ws_routes].
The payoff: #[use_ws_guards(...)], #[use_ws_pipes(...)], and #[use_ws_interceptors(...)] instances are built per message through resolve hooks on the traits — the WS mirror of HTTP CanActivate::resolve. Override resolve to pull injected dependencies; stateless types that only implement Default keep working unchanged:
ws_route family keep Default construction — a guard that expects injected dependencies silently receives an empty Default there. Mount registry-aware (via #[ws_gateway]) whenever a cross-cutting type needs DI.
Two more runtime hooks round out the picture:
WsGateway::message_guards()— return aWsGuardChainand the shared runtime runs it on every inbound message, on top of the#[use_ws_guards]checks compiled into the dispatch. Rejections emit anerrorframe and close the socket (guard status >= 500 closes with 1011, otherwise 1008).ws_route_with_guards(gateway, guards)— upgrade-timeWsUpgradeGuards run once on the HTTP upgrade request. A rejection accepts the upgrade and immediately closes the socket with 1008 (Policy Violation) plus the guard’s message, so WS clients observe an in-protocol rejection instead of an HTTP error they may not surface.
Error frame shapes
All errors are delivered on
WS_ERROR_EVENT ("error").
Origin checks and CSWSH protection
WebSocket upgrades are not covered by CORS — browsers send theOrigin header on the handshake, but nothing stops a malicious page from opening a WebSocket to your gateway and issuing calls as the victim (session cookies ride along automatically). This is Cross-Site WebSocket Hijacking (CSWSH). Defend with an explicit WsSecurityConfig origin allowlist.
Build a config that names every allowed origin and mount with ws_route_with_security:
- Allowlist entries are compared against the
Originheader verbatim — pass full origins including the scheme, e.g.https://app.example.com(no subdomain wildcards, no trailing-slash normalization). - A
nullOrigin (sandboxed iframes,file://, certain privacy contexts) is always rejected when any allowlist entry is configured. - An upgrade with no
Originheader (CLI tools, server-to-server clients) is accepted by default. Call.require_origin(true)on the config to reject bare-origin upgrades with HTTP 403 — useful for browser-only gateways:
WsSecurityConfig::allow_off()accepts every origin — the behavior of the legacyws_routeentry points, intended for gateways behind a trusted reverse proxy that enforces its own allowlist.
When both are present (
ws_route_with_guards_and_security(gateway, guards, security)), the security check runs first: a disallowed origin gets the 403; an allowed origin that fails a guard gets the in-protocol 1008 close.
Full WebSocket example
Full WebSocket example
Socket.IO
nestrs-ws speaks RFC 6455 JSON events. For the NestJS @nestjs/platform-socket.io protocol, see nestrs-socketio.