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.