Add a GraphQL endpoint with nestrs-graphql and async-graphql, or build real-time WebSocket gateways using nestrs-ws, #[ws_gateway], and #[subscribe_message].
nestrs provides two optional protocol extensions: the 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.
Define your query and mutation types using async-graphql attributes and build a Schema. Then pass it to enable_graphql:
use nestrs::prelude::*;use async_graphql::{Object, Schema, EmptyMutation, EmptySubscription};pub struct QueryRoot;#[Object]impl QueryRoot { async fn hello(&self) -> &str { "Hello from nestrs GraphQL" } async fn add(&self, a: i32, b: i32) -> i32 { a + b }}#[module]struct AppModule;#[tokio::main]async fn main() { let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription) .finish(); NestFactory::create::<AppModule>() .enable_graphql(schema) .listen(3000) .await;}
This mounts 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.
Federation, schema stitching, and codegen are outside the nestrs core. Use async-graphql’s federation support and Apollo Router or GraphOS to compose subgraphs. nestrs exposes a single /graphql endpoint — treat it as one subgraph in your platform.
For DataLoader / N+1 prevention, use async-graphql’s built-in dataloader feature or application-level batching in resolvers. The ecosystem is the same as a standalone async-graphql server.
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).
Apply cross-cutting to WebSocket handlers with the ws-specific attributes:
use nestrs::ws::{WsCanActivate, WsHandshake, WsGuardError};pub struct WsAuthGuard;impl Default for WsAuthGuard { fn default() -> Self { Self } }#[async_trait::async_trait]impl WsCanActivate for WsAuthGuard { async fn can_activate_ws( &self, handshake: &WsHandshake, _event: &str, _payload: &serde_json::Value, ) -> Result<(), WsGuardError> { let token = handshake .headers() .get("authorization") .and_then(|v| v.to_str().ok()); match token { Some(t) if t.starts_with("Bearer ") => Ok(()), _ => Err(WsGuardError::unauthorized("missing or invalid token")), } }}#[ws_gateway(path = "/ws")]#[use_ws_guards(WsAuthGuard)]pub struct ProtectedGateway;
WebSocket JSON frames do not flow through NestApplication::use_global_exception_filter or HttpException. Guard and pipe failures are sent to the client on the "error" event with statusCode, message, and error fields. There is no separate WsExceptionFilter trait in core today — centralize error handling by wrapping WsGateway::on_message or using shared guard/pipe types.