Connecting NextJS with NestJS via SocketIO

Introduction

SocketIO is a powerful library that enables bidirectional, real-time, event-based communication between the server and the browser.

The key advantages include:

  • Low Latency: Instant data transmission instead of having to send continuous requests.
  • Reliability: Automatic reconnection on connection loss and fallback support to HTTP long-polling if WebSocket is unavailable.
  • Broadcasting: Easily send data to one or multiple clients simultaneously.

In this article, I will guide you on using NextJS and NestJS to communicate with each other via SocketIO, simulating a chatbot app and the AI Agent's workflow.

Detail

On NestJS project you need to install these packages:

yarn add @nestjs/websockets @nestjs/platform-socket.io socket.io


Create the controller/conversations.controller.ts file returning the conversation list as follows:

import { Controller, Get } from "@nestjs/common";

@Controller("conversations")
export class ConversationsController {
@Get()
getConversations() {
return [
{
id: "1",
name: "NestJS System",
lastMsg: "Socket.io is ready",
time: "10:00",
online: true,
},
{
id: "2",
name: "React Squad",
lastMsg: "New components added",
time: "Yesterday",
online: false,
},
];
}
}


Create the socket/websocket.gateway.ts file:

import { MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from "@nestjs/websockets";
import { Server } from "socket.io";

@WebSocketGateway({
cors: { origin: "*" },
path: "/socket-io-ai",
})
export class ChatGateway {
@WebSocketServer()
server!: Server;

@SubscribeMessage("sendMessage")
async handleMessage(@MessageBody() data: { sender: string; text: string }) {
const steps = ["🔍 Analyzing user intent...", "📚 Querying internal knowledge base...", "🤖 Running inference with Llama-3...", "✨ Synthesizing final response..."];

for (const step of steps) {
await this.delay(600);
this.server.emit("ai_processing", { step });
}

const responseTemplates = [
`I've analyzed your message: "${data.text}". Based on my processing, everything looks optimal.`,
`Regarding "${data.text}", I've cross-referenced our database and found the relevant context for you.`,
`Processing "${data.text}" complete. I've utilized specialized logic to give you this insight.`,
`Interesting point about "${data.text}". My neural network suggests focusing on the core parameters mentioned.`,
`Task finished! I've successfully routed "${data.text}" through the NestJS Gateway and synthesized this result.`,
];

const randomIndex = Math.floor(Math.random() * responseTemplates.length);
const finalResponse = responseTemplates[randomIndex];

this.server.emit("ai_response", {
id: `ai-${Date.now()}`,
role: "ai",
content: finalResponse,
});
}

private delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

The code above initializes a WebSocket Gateway in NestJS, serving as an intermediary server. When receiving the "sendMessage" event, the server simulates the AI's Chain of Thought steps by sending consecutive "ai_processing" events with a delay, then returns the final result via the "ai_response" event.


Update the app.module.ts file:

import { Module } from "@nestjs/common";
import { ConversationsController } from "./controller/conversations.controller";
import { ChatGateway } from "./socket/websocket.gateway";

@Module({
controllers: [ConversationsController],
providers: [ChatGateway],
})
export class AppModule {}


On NextJS project you need to install the package:

yarn add socket.io-client


Create the app/chatbot/page.tsx file:

"use client";

import { LoadingOutlined, RobotOutlined, SendOutlined, UserOutlined } from "@ant-design/icons";
import { Avatar, Button, Input, Spin, Tag } from "antd";
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";

const NEST_BACKEND_URL = process.env.NEXT_PUBLIC_SOCKET_URL;

interface Message {
id: string;
role: "user" | "ai";
content: string;
steps?: string[];
}

export default function ChatPage() {
const [conversations, setConversations] = useState([]);
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const [isTyping, setIsTyping] = useState(false);
const [currentStep, setCurrentStep] = useState("");

const tempSteps = useRef<string[]>([]);
const socketRef = useRef<Socket | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);

useEffect(() => {
fetch(`${NEST_BACKEND_URL}/conversations`)
.then(res => res.json())
.then(setConversations)
.catch(err => console.error("Fetch error:", err));

socketRef.current = io(NEST_BACKEND_URL, {
path: "/socket-io-ai",
transports: ["websocket"],
});

socketRef.current.on("ai_processing", (data: { step: string }) => {
setIsTyping(true);
setCurrentStep(data.step);
tempSteps.current.push(data.step);
});

socketRef.current.on("ai_response", (data: Message) => {
const finalMsg: Message = {
...data,
steps: [...tempSteps.current],
};
setMessages(prev => [...prev, finalMsg]);
setIsTyping(false);
setCurrentStep("");
tempSteps.current = [];
});

return () => {
socketRef.current?.disconnect();
};
}, []);

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "smooth",
});
}
}, [messages, isTyping, currentStep]);

const handleSend = () => {
if (!inputValue.trim()) return;
const userMsg: Message = {
id: Date.now().toString(),
role: "user",
content: inputValue,
};
setMessages(prev => [...prev, userMsg]);
socketRef.current?.emit("sendMessage", { sender: "User", text: inputValue });
setInputValue("");
tempSteps.current = [];
};

return (
<div className="flex h-screen bg-slate-50 p-4 gap-4 overflow-hidden font-sans text-slate-900">
<div className="w-80 bg-white rounded-2xl shadow-sm border border-slate-200 flex flex-col overflow-hidden">
<div className="p-5 font-bold text-lg border-b border-slate-100 text-blue-600 bg-white uppercase tracking-wider">Conversations</div>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{conversations.map((item: any) => (
<div key={item.id} className="p-4 hover:bg-blue-50 cursor-pointer border-b border-slate-50 flex gap-3 items-center group transition-colors">
<div className="flex-shrink-0">
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.id}`} size={45} className="border border-slate-100" />
</div>
<div className="flex-1 overflow-hidden">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-slate-700 truncate">{item.name}</span>
<span className="text-[10px] text-slate-400 font-medium">{item.time}</span>
</div>
<div className="text-xs text-slate-500 truncate group-hover:text-blue-500 transition-colors">{item.lastMsg}</div>
</div>
</div>
))}
</div>
</div>

<div className="flex-1 bg-white rounded-2xl shadow-md border border-slate-200 flex flex-col overflow-hidden">
<div className="p-4 border-b border-slate-100 flex justify-between items-center bg-white/80 backdrop-blur-sm z-10">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<Avatar icon={<RobotOutlined />} className="bg-blue-600 shadow-sm" size={40} />
</div>
<div>
<div className="font-bold text-slate-800 leading-tight italic">AI Agentic Node</div>
<div className="text-[10px] text-blue-500 font-bold uppercase tracking-widest">Processing Online</div>
</div>
</div>
<Tag color="blue" className="rounded-full px-3 border-none bg-blue-50 text-blue-600 font-medium uppercase text-[10px]">
Socket.io Stream
</Tag>
</div>

<div ref={scrollRef} className="flex-1 overflow-y-auto p-6 space-y-8 bg-slate-50/50">
{messages.map(msg => (
<div key={msg.id} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"} animate-in fade-in duration-300`}>
<div className={`flex max-w-[85%] gap-3 ${msg.role === "user" ? "flex-row-reverse" : "flex-row"}`}>
<div className="flex-shrink-0 self-end mb-1">
<Avatar icon={msg.role === "user" ? <UserOutlined /> : <RobotOutlined />} className={`${msg.role === "user" ? "bg-slate-800" : "bg-blue-600"} shadow-sm`} size={32} />
</div>
<div className="flex flex-col gap-4">
{msg.role === "ai" && msg.steps && msg.steps.length > 0 && (
<div className="flex flex-col gap-2 ml-1">
<div className="flex items-center gap-2 opacity-60">
<div className="h-[1px] w-4 bg-slate-300"></div>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Chain of Thought</span>
</div>
<div className="flex flex-col gap-1.5 border-l-2 border-slate-100 ml-1 pl-4">
{msg.steps.map((step, idx) => (
<div key={idx} className="flex items-center gap-3 py-0.5 group">
<div className="w-1.5 h-1.5 rounded-full bg-blue-400/50 group-last:bg-blue-500 shadow-[0_0_8px_rgba(96,165,250,0.5)]"></div>
<span className="text-[12px] text-slate-500 italic font-medium leading-none tracking-tight">{step}</span>
</div>
))}
</div>
</div>
)}

<div
className={`p-4 rounded-2xl shadow-sm text-[14.5px] leading-relaxed tracking-tight ${
msg.role === "user" ? "bg-blue-600 text-white rounded-br-none shadow-blue-100" : "bg-white text-slate-700 border border-slate-100 rounded-tl-none font-medium"
}`}
>
{msg.content}
</div>
</div>
</div>
</div>
))}

{isTyping && (
<div className="flex justify-start gap-3 pl-11">
<div className="bg-white/80 backdrop-blur-sm px-4 py-2 rounded-2xl border border-blue-100 flex items-center gap-3 shadow-sm ring-4 ring-blue-50/50">
<span className="text-[12px] text-blue-600 font-bold italic tracking-wide animate-pulse">{currentStep || "Analyzing..."}</span>
<Spin indicator={<LoadingOutlined className="text-blue-600 text-sm" spin />} />
</div>
</div>
)}
</div>

<div className="p-5 bg-white border-t border-slate-100">
<div className="flex gap-2 p-1.5 border border-slate-200 rounded-2xl bg-slate-50 focus-within:bg-white focus-within:border-blue-500 focus-within:ring-4 focus-within:ring-blue-100/50 transition-all shadow-inner">
<Input
variant="borderless"
placeholder="Ask AI anything..."
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onPressEnter={handleSend}
disabled={isTyping}
className="py-2 px-4 text-slate-700 placeholder:text-slate-400"
/>
<Button
type="primary"
size="large"
shape="circle"
icon={<SendOutlined />}
onClick={handleSend}
disabled={!inputValue.trim() || isTyping}
className="flex-shrink-0 shadow-lg shadow-blue-200 flex items-center justify-center bg-blue-600 hover:scale-105 active:scale-95 transition-transform"
/>
</div>
</div>
</div>
</div>
);
}

This is the user interface (Client) using socket.io-client to listen for events from NestJS. When "ai_processing" is received, it displays the processing status and stores the steps in a temporary array. When "ai_response" is received, it updates the full content and the list of thought steps into the chat window for the user to follow.


Update the .env file; you can change the value if your server is at a different address:

NEXT_PUBLIC_SOCKET_URL = http://localhost:4000


The result is as follows:


You can see that the socket connection has been successfully created.

Happy coding!

See more articles here.

Comments