GraphQL with NestJS
Integrating GraphQL into a NestJS application using Apollo Server or other GraphQL libraries.
Real-time Updates with GraphQL Subscriptions in NestJS
Introduction to Real-time Updates with GraphQL Subscriptions
GraphQL Subscriptions enable real-time data updates to clients without constant polling. They are particularly useful for applications requiring immediate feedback, such as chat applications, live dashboards, collaborative tools, and real-time notifications. Instead of the client repeatedly asking the server for updates, the server pushes updates to subscribed clients whenever specific events occur. This approach is more efficient and provides a better user experience.
Explanation: GraphQL Subscriptions
GraphQL Subscriptions are a feature of the GraphQL specification that allows clients to subscribe to events on the server. When an event matching the subscription occurs, the server sends updated data to the client. This is achieved using a persistent connection, typically a WebSocket, between the client and the server.
Here's a breakdown of the key components:
- Subscription Definition: Like queries and mutations, subscriptions are defined in your GraphQL schema using a specific type. They specify the event that the client wants to subscribe to and the data they want to receive when that event occurs.
- WebSocket Transport: GraphQL Subscriptions often rely on the WebSocket protocol to establish a persistent, bidirectional communication channel between the client and the server.
- Subscription Resolver: A resolver function handles the subscription. This function typically returns an `AsyncIterator` (often created using RxJS Observables or similar asynchronous stream constructs). The server iterates over this stream, pushing data to subscribed clients whenever a new value is emitted.
- Event Trigger: Some event triggers the server to push data to the stream. This could be a database update, a message being posted to a chat room, or any other application-specific event.
Implementing GraphQL Subscriptions in NestJS
NestJS provides excellent support for implementing GraphQL Subscriptions through its @nestjs/graphql
module. Here's a step-by-step guide:
1. Install Necessary Packages
npm install @nestjs/graphql graphql apollo-server-express graphql-subscriptions
2. Configure GraphQL Module with Subscriptions
In your AppModule
(or your GraphQL module), configure the GraphQLModule
with a subscription definition:
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { PostsModule } from './posts/posts.module'; // Assuming you have a posts module
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: 'schema.gql', // Or use in-memory schema
installSubscriptionHandlers: true, // Enables subscriptions
playground: true, // Optional, for a GraphQL IDE
}),
PostsModule,
],
providers: [
{
provide: 'PUB_SUB', // Injectable for event publishing
useValue: new PubSub(),
},
],
})
export class AppModule {}
3. Define the GraphQL Schema with Subscription
Create or modify your GraphQL schema (schema.gql
or similar) to include a subscription definition:
type Post {
id: ID!
title: String!
content: String!
}
type Query {
posts: [Post!]!
}
type Mutation {
createPost(title: String!, content: String!): Post!
}
type Subscription {
postCreated: Post!
}
4. Create a Resolver for the Subscription
In your resolver (e.g., within your PostsResolver
), implement the subscription resolver. This uses an AsyncIterator
, typically created using RxJS or a similar library.
import { Resolver, Query, Mutation, Args, Subscription } from '@nestjs/graphql';
import { Post } from './models/post.model'; // Define your Post model
import { PostsService } from './posts.service'; // Your service for handling posts
import { PubSub } from 'graphql-subscriptions';
import { Inject } from '@nestjs/common';
import { NewPostInput } from './dto/new-post.input'; // Define your input type
@Resolver(() => Post)
export class PostsResolver {
constructor(
private readonly postsService: PostsService,
@Inject('PUB_SUB') private pubSub: PubSub,
) {}
@Query(() => [Post])
async posts(): Promise {
return this.postsService.findAll();
}
@Mutation(() => Post)
async createPost(@Args('newPostData') newPostData: NewPostInput): Promise {
const post = await this.postsService.create(newPostData);
this.pubSub.publish('postCreated', { postCreated: post }); // Publish the event
return post;
}
@Subscription(() => Post, {
name: 'postCreated',
})
postCreated() {
return this.pubSub.asyncIterator('postCreated'); // Subscribe to the event
}
}
5. Service Logic and Data Storage
Your PostsService
would handle the data storage and retrieval. This could involve interacting with a database or any other data source.
import { Injectable } from '@nestjs/common';
import { Post } from './models/post.model';
import { NewPostInput } from './dto/new-post.input';
@Injectable()
export class PostsService {
private readonly posts: Post[] = [];
private nextId = 1;
async create(data: NewPostInput): Promise {
const post: Post = {
id: this.nextId.toString(),
title: data.title,
content: data.content,
};
this.posts.push(post);
this.nextId++;
return post;
}
async findAll(): Promise {
return this.posts;
}
async findOne(id: string): Promise {
return this.posts.find(post => post.id === id);
}
}
6. Define DTOs and Models
Create the necessary DTOs (Data Transfer Objects) and Models. Example:
// src/posts/dto/new-post.input.ts
import { InputType, Field } from '@nestjs/graphql';
import { IsNotEmpty, MaxLength } from 'class-validator';
@InputType()
export class NewPostInput {
@Field()
@IsNotEmpty()
@MaxLength(30)
title: string;
@Field()
@IsNotEmpty()
content: string;
}
// src/posts/models/post.model.ts
import { ObjectType, Field, ID } from '@nestjs/graphql';
@ObjectType()
export class Post {
@Field(() => ID)
id: string;
@Field()
title: string;
@Field()
content: string;
}
7. WebSocket Configuration (Optional, but important for more complex scenarios)
While installSubscriptionHandlers: true
in GraphQLModule.forRoot()
handles basic WebSocket setup, you might need more control. For example, you might need custom authentication or more advanced WebSocket options. You can customize the WebSocket adapter:
// Create a WebSocket adapter (example using `ws`)
import { IoAdapter } from '@nestjs/platform-socket.io';
import { INestApplicationContext } from '@nestjs/common';
import { ServerOptions } from 'socket.io';
import { Socket } from 'socket.io';
export class WsAdapter extends IoAdapter {
constructor(private app: INestApplicationContext) {
super(app);
}
createIOServer(port: number, options?: ServerOptions): any {
const server = super.createIOServer(port, options);
server.on('connection', (socket: Socket) => {
// Implement your authentication or other custom logic here
console.log('Client connected:', socket.id);
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
return server;
}
}
// Then, in your main.ts (or bootstrapping file):
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { WsAdapter } from './ws.adapter'; // Import your custom adapter
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app)); // Use the custom adapter
await app.listen(3000);
}
bootstrap();
8. Client-Side Implementation
On the client-side, you'll use a GraphQL client that supports subscriptions, such as Apollo Client or urql. Here's a basic Apollo Client example:
// Install the Apollo Client packages:
// npm install @apollo/client graphql subscriptions-transport-ws ws
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql', // Your GraphQL endpoint
});
const wsLink = new WebSocketLink({
uri: 'ws://localhost:3000/graphql', // Your WebSocket endpoint
options: {
reconnect: true,
},
webSocketImpl: WebSocket // Important for Node.js environments
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
// Now you can use this client to execute subscriptions:
import { gql, useSubscription } from '@apollo/client';
const POST_CREATED = gql`
subscription PostCreated {
postCreated {
id
title
content
}
}
`;
function NewPosts() {
const { data, loading, error } = useSubscription(POST_CREATED);
if (loading) return Loading...
;
if (error) return Error: {error.message}
;
return (
<div>
<h2>New Posts:</h2>
<ul>
{data.postCreated && (
<li key={data.postCreated.id}>
{data.postCreated.title} - {data.postCreated.content}
</li>
)}
</ul>
</div>
);
}
Conclusion
GraphQL Subscriptions in NestJS provide a powerful and efficient way to implement real-time updates in your applications. By leveraging WebSockets and asynchronous streams, you can deliver a superior user experience with immediate feedback and minimal overhead.