← Back to articles

Building a Realtime Chat App with NestJs, GraphQL Subscriptions, and WebSockets: A Practical Guide

October 6, 2026 · 5 min read

Building a Realtime Chat App with NestJs, GraphQL Subscriptions, and WebSockets: A Practical Guide

Realtime communication is a fundamental feature in modern applications, especially chat apps where updates need to flow instantly between users. NestJs provides a powerful backend framework for building scalable server-side apps, and when combined with GraphQL subscriptions over WebSockets, it enables elegant realtime capabilities. This article walks you through the process of creating a realtime chat backend with NestJs, GraphQL subscriptions, and WebSockets, covering key implementation details, common pitfalls, and best practices.

Why GraphQL Subscriptions with NestJs?

WebSocket-based subscriptions allow clients to maintain a persistent connection to a GraphQL server and receive push notifications when data changes, perfect for chat and notifications. NestJs's integration with Apollo and its modular architecture simplifies wiring up GraphQL subscriptions alongside regular queries and mutations.

By the end of this guide, you'll understand how to:

  • Set up WebSocket transport in a NestJs GraphQL server
  • Define subscription resolvers and triggers
  • Handle PubSub mechanisms securely and scalably
  • Troubleshoot common WebSocket pitfalls

Setting up the NestJs Project with GraphQL

First, initialize a NestJs app if you haven't already:

npm i -g @nestjs/cli
nest new chat-backend

Install GraphQL and related packages:

npm install --save @nestjs/graphql graphql apollo-server-express graphql-subscriptions

Additionally, for WebSocket support ensure subscriptions-transport-ws (for Apollo v2) or use Apollo Server 3+ which has built-in support. Here, we use the Apollo driver bundled with NestJs.

Modify app.module.ts to set up GraphQL with subscriptions enabled:

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { ChatModule } from './chat/chat.module';

@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      installSubscriptionHandlers: true, // Critical for subscriptions
      playground: true,
    }),
    ChatModule,
  ],
})
export class AppModule {}

Designing the Chat Schema with Subscriptions

Use the GraphQL schema-first or code-first approach (NestJs supports both), here is a simple chat schema:

type Message {
  id: ID!
  content: String!
  sender: String!
  timestamp: String!
}

type Query {
  messages: [Message!]!
}

type Mutation {
  sendMessage(content: String!, sender: String!): Message!
}

type Subscription {
  messageSent: Message!
}

The messageSent subscription emits new messages whenever they are sent.

Implementing PubSub with graphql-subscriptions

GraphQL subscriptions rely on a PubSub mechanism. For prototyping and small scale, the in-memory PubSub from graphql-subscriptions suffices:

npm install graphql-subscriptions

Create a shared service:

import { Injectable } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';

@Injectable()
export class PubSubService {
  public pubSub = new PubSub();
}

This service is injected into your chat resolver to publish and subscribe to events.

Writing the Chat Resolver

In chat.resolver.ts:

import { Resolver, Query, Mutation, Args, Subscription } from '@nestjs/graphql';
import { PubSubService } from '../pubsub/pubsub.service';
import { Message } from './message.model';
import { UseFilters } from '@nestjs/common';

const MESSAGE_SENT_EVENT = 'messageSent';

@Resolver(() => Message)
export class ChatResolver {
  private messages: Message[] = [];

  constructor(private readonly pubSubService: PubSubService) {}

  @Query(() => [Message])
  messages() {
    return this.messages;
  }

  @Mutation(() => Message)
  sendMessage(
    @Args('content') content: string,
    @Args('sender') sender: string,
  ) {
    const message: Message = {
      id: Date.now().toString(),
      content,
      sender,
      timestamp: new Date().toISOString(),
    };
    this.messages.push(message);
    this.pubSubService.pubSub.publish(MESSAGE_SENT_EVENT, { messageSent: message });
    return message;
  }

  @Subscription(() => Message, {
    resolve: (value) => value,
  })
  messageSent() {
    return this.pubSubService.pubSub.asyncIterator(MESSAGE_SENT_EVENT);
  }
}

Common Pitfalls and How to Avoid Them

  • installSubscriptionHandlers missing: Make sure this option is true in forRoot() to enable WebSocket subscriptions.
  • PubSub usage: The in-memory PubSub isn't suitable for distributed systems because it doesn't share events across multiple instances.
  • WebSocket connection issues: If clients can't receive subscription data, verify network proxies or firewalls aren't blocking WebSocket traffic.
  • Subscription filtering: Subscriptions can be filtered to only send relevant data to clients. Without filtering, all subscribers may get irrelevant messages causing unnecessary load.

For filtering example:

@Subscription(() => Message, {
  filter: (payload, variables) => {
    return payload.messageSent.sender === variables.sender;
  },
})
messageSent(@Args('sender') sender: string) {
  return this.pubSubService.pubSub.asyncIterator(MESSAGE_SENT_EVENT);
}

Scaling Subscriptions

When scaling horizontally (multiple app instances), the in-memory PubSub causes message events to be local per instance, resulting in inconsistent subscription events. Consider using:

  • Redis-based PubSub implementations (graphql-redis-subscriptions)
  • Message queues (RabbitMQ, Kafka) for event propagation

Example with Redis PubSub:

import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const options = {
  host: 'localhost',
  port: 6379,
};

const pubSub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});

Integrate this PubSub in your NestJs service for scaled deployments.

Testing Your Subscriptions

Use GraphQL Playground (enabled in our setup) or tools like Insomnia and Altair to:

  1. Run a query subscription:
subscription {
  messageSent {
    id
    content
    sender
  }
}
  1. Send mutations:
mutation {
  sendMessage(content: "Hello", sender: "Alice") {
    id
    content
    sender
  }
}

Ensure subscription clients receive real-time data push.

Security Considerations

  • Authentication: Authenticate WebSocket connections using context or onConnect handlers. Avoid including sensitive tokens in subscription payloads.
  • Authorization: Implement schema-level guards or middleware to restrict subscription access.

Example of passing auth token during connection:

GraphQLModule.forRoot({
  installSubscriptionHandlers: true,
  subscriptions: {
    'subscriptions-transport-ws': {
      onConnect: (connectionParams) => {
        if (!connectionParams.authToken) {
          throw new Error('Missing auth token!');
        }
        return { user: getUserFromToken(connectionParams.authToken) };
      },
    },
  },
});

Access authenticated user in resolvers via context.


Key Takeaways

  • NestJs simplifies GraphQL and WebSocket integration, making realtime subscription features accessible.
  • Enable installSubscriptionHandlers and use a PubSub system for pushing events.
  • The built-in in-memory PubSub is fine for demos/small apps but replace with Redis or other systems for production scaling.
  • Test carefully with appropriate clients and handle authentication/authorization for subscription connections.
  • Applying filters on subscriptions can optimize network bandwidth and user experience.

Building a chat backend with realtime subscriptions in NestJs equips you with a powerful pattern for other realtime features. With attention to scaling and security, you can extend this foundation to robust production-grade applications.