Home Blog Page 109

Setting Up GraphQL with Code-First and Schema-First Approaches in NestJS

0
nestjs fullstack course
nestjs fullstack course

GraphQL is a query language for APIs that allows clients to request exactly the data they need, making it an efficient alternative to RESTful APIs. NestJS provides seamless integration with GraphQL, supporting two main approaches to defining GraphQL schemas: Code-First and Schema-First.

In this module, we will explore both approaches, learn how to set up GraphQL in a NestJS application, and compare the pros and cons of each method.


Table of Contents

  1. What is GraphQL?
  2. Setting Up GraphQL in NestJS
  3. Code-First Approach
  4. Schema-First Approach
  5. Comparing Code-First vs. Schema-First
  6. Best Practices
  7. Conclusion

What is GraphQL?

GraphQL is a query language and runtime for executing queries against your data. Unlike traditional REST APIs, which return predefined data from multiple endpoints, GraphQL allows you to query only the data you need, which reduces over-fetching and under-fetching.

Key features of GraphQL include:

  • Strongly typed schema: It defines the types of data and how they can be queried.
  • Single endpoint: Unlike REST APIs, which use multiple endpoints, GraphQL uses one endpoint for all requests.
  • Real-time updates: With subscriptions, GraphQL can provide real-time updates.

Setting Up GraphQL in NestJS

To use GraphQL in a NestJS application, you’ll need to install the necessary dependencies and configure the module. Here’s how to get started:

Step 1: Install Dependencies

npm install @nestjs/graphql graphql-tools graphql apollo-server-express

Step 2: Configure GraphQL in NestJS

In your main module (typically app.module.ts), configure the GraphQL module.

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UserModule } from './user/user.module';

@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: true, // auto-generate the schema file
playground: true, // enable GraphQL playground for testing queries
}),
UserModule,
],
})
export class AppModule {}

This setup enables automatic schema generation and the GraphQL playground for interactive query testing.


Code-First Approach

The Code-First approach in GraphQL means that the schema is generated directly from TypeScript code using decorators. This approach leverages NestJS’s powerful decorators and makes it easy to define and maintain the schema directly in your code.

Installing Dependencies

To use the Code-First approach, you will need the following dependencies (which we’ve already installed):

  • @nestjs/graphql: The NestJS wrapper around GraphQL.
  • graphql: The GraphQL library for Node.js.
  • @nestjs/apollo-server-express: Apollo server integration for NestJS.

Creating the GraphQL Schema

In the Code-First approach, you define GraphQL types using decorators provided by @nestjs/graphql.

// user.model.ts
import { ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class User {
@Field(type => Int)
id: number;

@Field()
name: string;

@Field()
email: string;
}

Here, the @ObjectType decorator defines a GraphQL object type, and the @Field decorator specifies the fields of that type.

Resolvers and Types

Resolvers in the Code-First approach are used to fetch data for a specific query or mutation.

// user.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { User } from './user.model';
import { UserService } from './user.service';

@Resolver(of => User)
export class UserResolver {
constructor(private userService: UserService) {}

@Query(returns => User)
async getUser(@Args('id') id: number): Promise<User> {
return this.userService.findOne(id);
}

@Mutation(returns => User)
async createUser(@Args('name') name: string, @Args('email') email: string): Promise<User> {
return this.userService.create(name, email);
}
}

In this example:

  • The @Query decorator defines a GraphQL query.
  • The @Mutation decorator defines a GraphQL mutation.

Schema-First Approach

The Schema-First approach means that the GraphQL schema is manually defined using the SDL (Schema Definition Language), and then NestJS resolvers are written to match the schema. This approach gives you more control over the schema but requires manually maintaining the schema and keeping it in sync with your resolvers.

Installing Dependencies

For the Schema-First approach, the dependencies remain the same as for Code-First, but the main difference lies in how you define the schema.

Defining the Schema

You define your schema in a .graphql file using the Schema Definition Language (SDL):

# schema.graphql
type User {
id: Int!
name: String!
email: String!
}

type Query {
getUser(id: Int!): User
}

type Mutation {
createUser(name: String!, email: String!): User
}

Resolvers and Types

You then define resolvers to implement the schema.

// user.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { UserService } from './user.service';
import { User } from './user.model';

@Resolver('User')
export class UserResolver {
constructor(private userService: UserService) {}

@Query('getUser')
async getUser(@Args('id') id: number): Promise<User> {
return this.userService.findOne(id);
}

@Mutation('createUser')
async createUser(@Args('name') name: string, @Args('email') email: string): Promise<User> {
return this.userService.create(name, email);
}
}

Here, the @Resolver decorator refers to the User type defined in the SDL schema, and the resolvers are implemented in the same way as in the Code-First approach.


Comparing Code-First vs. Schema-First

FeatureCode-FirstSchema-First
Schema DefinitionAutomatically generated from codeDefined manually using SDL
FlexibilityEasier to maintain, less manual effortMore control over schema structure
Type SafetyStrongly typed via TypeScriptType checking requires additional setup
Developer ExperienceSeamless integration with NestJS decoratorsBetter suited for larger teams and existing schemas
Learning CurveEasier for beginners to understandMay require more upfront work to learn SDL

Best Practices

  • For small to medium projects: The Code-First approach is typically faster and more convenient, especially with NestJS’s powerful decorators.
  • For large projects or teams: The Schema-First approach offers more control over the schema and can be more suitable if you are working with external systems or need to maintain a consistent schema across multiple applications.
  • Keep resolvers simple: Avoid adding business logic in resolvers. Keep them focused on fetching and mutating data.
  • Type safety: Use the @nestjs/graphql decorators to enforce strict typing, ensuring better developer experience and fewer bugs.

Conclusion

NestJS makes it simple to integrate GraphQL into your application, whether you prefer the Code-First approach or the Schema-First approach. Both approaches are supported out of the box, and the choice depends on the complexity of your project and your team’s needs. Code-First is perfect for rapid development and small teams, while Schema-First offers more control for larger teams and complex systems.

Working with Background Jobs using Bull and Redis in NestJS

0
nestjs fullstack course
nestjs fullstack course

Background jobs are crucial for offloading resource-intensive tasks, such as email sending, image processing, or data synchronization, from the main request-response cycle. Bull, a popular job and task queue library, is integrated with Redis to manage these background jobs in a scalable and fault-tolerant way.

In this module, we’ll explore how to use Bull and Redis in a NestJS application to implement efficient and reliable background job processing.


Table of Contents

  1. What Are Background Jobs?
  2. Why Use Bull and Redis in NestJS?
  3. Installing Bull and Redis Packages
  4. Setting Up Bull in NestJS
  5. Creating Job Processors
  6. Handling Job Events
  7. Scheduling Jobs
  8. Monitoring Jobs
  9. Best Practices and Security
  10. Conclusion

What Are Background Jobs?

Background jobs allow you to perform long-running or resource-heavy operations asynchronously, outside of the main request/response cycle. This improves the responsiveness of your app, prevents timeouts, and makes it more scalable.

Common background job use cases include:

  • Sending emails or notifications.
  • Processing uploaded files (e.g., images, videos).
  • Syncing data between services.
  • Running scheduled tasks like cleanup or backups.

Why Use Bull and Redis in NestJS?

Bull is a queue system for handling background jobs, while Redis serves as a fast, in-memory data store to keep track of job states.

Here’s why Bull and Redis are ideal for background job management in NestJS:

  • Scalability: Redis can handle millions of jobs across multiple instances.
  • Fault Tolerance: Bull provides retries, delays, and job scheduling.
  • Ease of Use: Bull’s API is simple and integrates seamlessly with NestJS.

By combining Bull with NestJS, you can offload time-consuming tasks and still maintain application performance.


Installing Bull and Redis Packages

To get started, install the required dependencies:

npm install @nestjs/bull bull redis
npm install --save-dev @types/bull

Then, add BullModule to your application module:

// app.module.ts
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { JobProcessorService } from './job-processor.service';

@Module({
imports: [
BullModule.forRoot({
redis: {
host: 'localhost',
port: 6379,
},
}),
BullModule.registerQueue({
name: 'jobQueue',
}),
],
providers: [JobProcessorService],
})
export class AppModule {}

Setting Up Bull in NestJS

You need to define the job queue and set up a worker to process the background jobs. Let’s create a queue that handles a simple task (e.g., sending emails).

Step 1: Define a Job Queue

// job-processor.service.ts
import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';

@Injectable()
export class JobProcessorService {
constructor(@InjectQueue('jobQueue') private jobQueue: Queue) {}

async addJob(data: any) {
// Adding a job to the queue
await this.jobQueue.add('sendEmail', data);
}
}

Step 2: Create a Job Processor

// job-processor.processor.ts
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
import { Injectable } from '@nestjs/common';

@Processor('jobQueue')
@Injectable()
export class JobProcessor {
@Process('sendEmail')
async handleSendEmailJob(job: Job) {
const { email, subject, message } = job.data;
console.log(`Sending email to ${email} with subject: ${subject}`);

// Logic for sending email
// await this.mailService.sendEmail(email, subject, message);
}
}

Creating Job Processors

A job processor is responsible for processing jobs from the queue. You can create multiple job processors for different tasks, each associated with specific job types.

In the example above, the sendEmail job type is processed by the handleSendEmailJob method. You can create other methods for different types of jobs, such as image processing or file uploads.


Handling Job Events

Bull provides several event hooks to monitor the state of jobs, such as when a job is completed, failed, or delayed.

Example: Handling Job Completion and Failure

// job-processor.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';

@Injectable()
export class JobProcessorService implements OnModuleInit {
constructor(@InjectQueue('jobQueue') private jobQueue: Queue) {}

async onModuleInit() {
this.jobQueue.on('completed', (job) => {
console.log(`Job completed: ${job.id}`);
});

this.jobQueue.on('failed', (job, err) => {
console.error(`Job failed: ${job.id} with error: ${err.message}`);
});
}
}

Scheduling Jobs

Bull allows you to schedule jobs to run at specific intervals. This is useful for recurring tasks like sending daily reports or backups.

// job-processor.service.ts
async addScheduledJob() {
await this.jobQueue.add(
'sendDailyReport',
{ reportType: 'daily' },
{
repeat: { cron: '0 0 * * *' }, // Runs every day at midnight
},
);
}

This example adds a job to be repeated daily at midnight, using the Cron syntax.


Monitoring Jobs

To ensure smooth operation of your background jobs, you can monitor them using Bull’s UI and the Bull Board package.

npm install bull-board

Then, add the Bull Board UI in your main.ts:

import { BullModule } from '@nestjs/bull';
import { BullBoard } from 'bull-board';
import { createBullBoard } from 'bull-board';
import { BullAdapter } from 'bull-board/dist/bullAdapter';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

const serverAdapter = new BullAdapter(jobQueue);
const bullBoard = createBullBoard({
queues: [serverAdapter],
});

app.use('/admin/queues', bullBoard.router);
await app.listen(3000);
}
bootstrap();

Best Practices and Security

  • Job Retries: Configure job retries for transient failures.
  • Job Prioritization: Use priorities to process critical jobs first.
  • Error Handling: Add proper error handling within job processors and listeners.
  • Security: Use Redis authentication for secure connections, especially when using Redis in a production environment.

Conclusion

By using Bull and Redis in your NestJS application, you can implement efficient and scalable background job processing. With the ability to schedule tasks, handle job events, and monitor job statuses, you can ensure that long-running tasks do not block the main application thread, providing a better user experience and improving overall application performance.

Using Event Emitters and Async Event Handling in NestJS

0
nestjs fullstack course
nestjs fullstack course

Event-driven architecture is a powerful design pattern that promotes loose coupling, scalability, and better organization of logic. In NestJS, you can leverage the built-in event emitter system to handle events asynchronously within your application.

In this module, we’ll explore how to use @nestjs/event-emitter to emit and handle events, process them asynchronously, and integrate this mechanism cleanly into your NestJS apps.


Table of Contents

  1. What Are Event Emitters in NestJS?
  2. When to Use Event Emitters
  3. Installing @nestjs/event-emitter
  4. Emitting Events
  5. Listening to Events with Listeners
  6. Async Event Handling
  7. Real-World Use Cases
  8. Best Practices
  9. Conclusion

What Are Event Emitters in NestJS?

NestJS offers first-class support for event-driven patterns using the @nestjs/event-emitter package. This allows different parts of your application to communicate indirectly through events, making your architecture more modular and decoupled.

Think of it as the pub-sub pattern:

  • Emitters publish events.
  • Listeners subscribe to and respond to events.

When to Use Event Emitters

Use event emitters when:

  • You want to decouple concerns (e.g., send a welcome email after registration).
  • You want to trigger background tasks asynchronously.
  • You need to notify other parts of your app without circular dependencies.

Installing @nestjs/event-emitter

Install the official event emitter module:

npm install @nestjs/event-emitter

Then import the module in your root or feature module:

// app.module.ts
import { Module } from '@nestjs/common';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { UserModule } from './user/user.module';

@Module({
imports: [
EventEmitterModule.forRoot(), // globally sets up emitter
UserModule,
],
})
export class AppModule {}

Emitting Events

You can inject EventEmitter2 and emit custom events from any service.

// user.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class UserService {
constructor(private eventEmitter: EventEmitter2) {}

async registerUser(data: any) {
// logic to create user
const user = { id: 1, email: data.email };

// Emit event after user is registered
this.eventEmitter.emit('user.registered', user);
}
}

You can emit any object, payload, or metadata you want.


Listening to Events with Listeners

Now you need a listener to react to the emitted event.

// user.listener.ts
import { OnEvent } from '@nestjs/event-emitter';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserListener {
@OnEvent('user.registered')
handleUserRegisteredEvent(payload: any) {
console.log(`User registered:`, payload);
// e.g., send welcome email or log activity
}
}

Make sure to register the listener in your module:

// user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserListener } from './user.listener';

@Module({
providers: [UserService, UserListener],
})
export class UserModule {}

Async Event Handling

Event handlers can be asynchronous by returning a Promise.

@OnEvent('user.registered')
async handleUserRegisteredEvent(payload: any) {
await this.mailService.sendWelcomeEmail(payload.email);
}

You can also define event priorities and set wildcards for broader patterns.

Example with wildcard:

@OnEvent('user.*')
handleAllUserEvents(payload: any) {
console.log('User event occurred:', payload);
}

Real-World Use Cases

  • User registration → Send welcome emails.
  • Order placement → Send confirmation and trigger invoice generation.
  • Password reset → Log the attempt and notify via email.
  • Notifications → Trigger real-time socket events based on app activity.

Best Practices

  • Use strongly typed event payloads for better maintainability.
  • Don’t perform heavy tasks directly in listeners—offload to background jobs if needed.
  • Avoid overusing emitters for operations that should be handled via service method calls (keep them purposeful).
  • Structure listeners in separate files/modules for clarity.

Conclusion

Event Emitters in NestJS provide a powerful abstraction for decoupling your application logic and implementing reactive flows. By emitting and listening to events, you make your application more modular, maintainable, and ready for scalability.

Redis Pub/Sub with WebSockets for Scalable Messaging in NestJS

0
nestjs fullstack course
nestjs fullstack course

As your real-time application grows, a single WebSocket server may not be able to handle all clients efficiently. In a distributed system where multiple instances of your NestJS app are running, messages need to be shared across them seamlessly. This is where Redis Pub/Sub comes into play.

In this module, you’ll learn how to integrate Redis with NestJS WebSockets to enable scalable real-time communication using the publish/subscribe pattern.


Table of Contents

  1. Why Use Redis Pub/Sub with WebSockets?
  2. How Pub/Sub Works in Redis
  3. Installing Required Packages
  4. Setting Up Redis Pub/Sub in NestJS
  5. Integrating with WebSocket Gateway
  6. Broadcasting Across Instances
  7. Testing the Scalable Architecture
  8. Security and Best Practices
  9. Conclusion

Why Use Redis Pub/Sub with WebSockets?

When your application scales horizontally (multiple Node.js instances), Socket.IO alone can’t keep all clients in sync. For example, if user A is connected to Instance 1 and user B is on Instance 2, messages between them won’t be delivered unless the instances can share messages.

Redis Pub/Sub acts as a centralized broker to:

  • Publish messages from any instance
  • Distribute messages to all subscribing instances
  • Maintain real-time consistency across distributed systems

How Pub/Sub Works in Redis

The Publisher sends a message to a Redis channel.
The Subscribers (your app instances) listen to that channel and receive the message.

It’s a perfect pattern for broadcasting WebSocket messages across clusters or Docker containers.


Installing Required Packages

You’ll need the following:

npm install redis socket.io-redis
npm install --save-dev @types/redis

For NestJS with Socket.IO adapter:

npm install @nestjs/platform-socket.io @nestjs/websockets

Setting Up Redis Pub/Sub in NestJS

Use socket.io-redis to create a custom adapter.

Step 1: Create a Redis Adapter

// redis.adapter.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

export class RedisIoAdapter extends IoAdapter {
async createIOServer(port: number, options?: ServerOptions): Promise<any> {
const server = super.createIOServer(port, options);

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

await pubClient.connect();
await subClient.connect();

server.adapter(createAdapter(pubClient, subClient));

return server;
}
}

Step 2: Apply the Adapter in main.ts

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './redis.adapter';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new RedisIoAdapter(app));
await app.listen(3000);
}
bootstrap();

Integrating with WebSocket Gateway

Your WebSocket gateway doesn’t change much. Redis automatically handles message routing between app instances.

@WebSocketGateway({ cors: true })
export class ChatGateway {
@WebSocketServer()
server: Server;

@SubscribeMessage('sendMessage')
handleMessage(
@MessageBody() payload: { room: string; message: string },
@ConnectedSocket() client: Socket,
) {
this.server.to(payload.room).emit('receiveMessage', {
user: client.id,
message: payload.message,
});
}
}

Broadcasting Across Instances

When a client sends a message, it’s published to Redis. Redis then forwards it to all other connected clients, regardless of the instance they’re on.

This enables:

  • Load balancing WebSocket traffic
  • Real-time chat apps with rooms
  • Microservices communication via WebSockets

Testing the Scalable Architecture

You can simulate this setup by:

  1. Running multiple instances of your NestJS app (e.g., on different ports)
  2. Connecting one client to each instance
  3. Sending a message from one client and verifying the other receives it

You’ll observe that Redis ensures real-time delivery even across app instances.


Security and Best Practices

  • Namespace Isolation: Use namespaces for multi-feature apps (/chat, /notifications, etc.).
  • Authentication: Validate JWT or sessions during connection using middleware or guards.
  • Error Handling: Gracefully manage Redis disconnections or failures.
  • Scaling: Redis handles a lot, but monitor performance under load.

Conclusion

Integrating Redis Pub/Sub with NestJS WebSockets provides a robust foundation for scalable real-time systems. Whether you’re building chat apps, collaboration tools, or live dashboards, Redis ensures consistency and performance in distributed environments.

Building a Chat App with Gateway, Rooms, and Events in NestJS

0
nestjs fullstack course
nestjs fullstack course

Real-time chat applications are a perfect use case for WebSockets. In this module, we’ll build a basic chat app using NestJS WebSocket Gateways, Socket.IO rooms, and custom events. You’ll learn how to structure a real-time communication system with NestJS that supports multiple users and chat rooms.


Table of Contents

  1. Overview of Real-Time Chat Features
  2. Setting Up the WebSocket Gateway
  3. Creating Chat Rooms Using Socket.IO
  4. Managing Users and Broadcasting Events
  5. Frontend Socket.IO Client
  6. Handling Room Events and Messages
  7. Bonus: Persisting Messages (Optional)
  8. Conclusion

Overview of Real-Time Chat Features

In our chat app, users can:

  • Connect via WebSocket
  • Join specific rooms (like channels)
  • Send messages to others in the same room
  • Receive messages in real-time
  • Optionally persist messages in a database

We’ll use:

  • @nestjs/websockets for gateway setup
  • socket.io for client-server real-time communication
  • Simple in-memory logic for rooms and broadcasting

Setting Up the WebSocket Gateway

Install the required packages if you haven’t already:

npm install @nestjs/websockets @nestjs/platform-socket.io socket.io

Create a WebSocket Gateway:

// chat.gateway.ts
import {
WebSocketGateway,
WebSocketServer,
SubscribeMessage,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({ cors: true })
export class ChatGateway {
@WebSocketServer()
server: Server;

@SubscribeMessage('joinRoom')
handleJoinRoom(
@MessageBody() room: string,
@ConnectedSocket() client: Socket,
) {
client.join(room);
client.emit('joinedRoom', `Joined room: ${room}`);
}

@SubscribeMessage('sendMessage')
handleMessage(
@MessageBody() payload: { room: string; message: string },
@ConnectedSocket() client: Socket,
) {
const { room, message } = payload;
this.server.to(room).emit('receiveMessage', {
user: client.id,
message,
});
}
}

Creating Chat Rooms Using Socket.IO

With Socket.IO, creating rooms is simple. You use client.join(roomName) to add a client to a room. Rooms can be dynamic and identified by chat group names, user IDs, or custom IDs.

client.join('dev-room'); // Adds client to the 'dev-room'

Messages can then be scoped to a room:

this.server.to('dev-room').emit('receiveMessage', payload);

Managing Users and Broadcasting Events

To enhance functionality, you can track connected users and rooms using a simple map or a database.

Example:

const activeUsers: Record<string, string> = {}; // clientId -> username

@SubscribeMessage('register')
handleRegister(
@MessageBody() username: string,
@ConnectedSocket() client: Socket,
) {
activeUsers[client.id] = username;
client.emit('registered', `Welcome, ${username}`);
}

When a user sends a message:

this.server.to(room).emit('receiveMessage', {
user: activeUsers[client.id] || client.id,
message,
});

Frontend Socket.IO Client

<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
const socket = io('http://localhost:3000');

socket.emit('joinRoom', 'room1');

socket.on('joinedRoom', (msg) => {
console.log(msg);
});

socket.emit('sendMessage', {
room: 'room1',
message: 'Hello Room!',
});

socket.on('receiveMessage', (msg) => {
console.log(`[${msg.user}]: ${msg.message}`);
});
</script>

Handling Room Events and Messages

You can extend your gateway to handle:

  • User typing indicators
  • Message read receipts
  • User disconnection announcements
  • Notifications for new users

Example: Notify room on user join

handleJoinRoom(room: string, client: Socket) {
client.join(room);
this.server.to(room).emit('userJoined', {
user: client.id,
room,
});
}

Bonus: Persisting Messages (Optional)

For production apps, persist messages using a database:

  1. Create a Message entity/model
  2. Use a MessageService to save chat data
  3. Call the service in handleMessage
await this.messageService.save({
room,
userId: client.id,
message,
timestamp: new Date(),
});

Conclusion

You’ve now built a simple but powerful chat app using NestJS Gateways and Socket.IO. You’ve learned how to:

  • Create and manage chat rooms
  • Handle real-time message delivery
  • Build a frontend that interacts with the backend
  • Optionally persist chat messages