Next.js с WebSockets и Socket.io

16 октября 2026
Автор

Антон Ларичев

Что такое WebSockets и зачем они нужны в Next.js

WebSocket — протокол двусторонней связи между клиентом и сервером через единственное постоянное соединение. В отличие от HTTP, где клиент всегда инициирует запрос, WebSocket позволяет серверу самостоятельно отправлять данные в любой момент.

Это делает WebSockets незаменимыми для:

  • Чат-приложений и мессенджеров
  • Уведомлений в реальном времени
  • Совместного редактирования документов
  • Онлайн-игр и торговых терминалов
  • Отслеживания геолокации

Socket.io — популярная библиотека, которая надстраивается над WebSocket и добавляет автоматическое переподключение, комнаты, пространства имён и фолбэк на long-polling при отсутствии поддержки WebSocket.

Курсы PurpleSchool

Учитесь у практикующих разработчиков — без воды

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Ограничения стандартного подхода в Next.js

Стандартные Route Handlers в Next.js возвращают ответ и завершают соединение — они не рассчитаны на долгоживущие соединения WebSocket.

Есть два основных подхода к интеграции Socket.io с Next.js:

  1. Пользовательский сервер (Custom Server) — запуск отдельного Node.js-сервера рядом с Next.js
  2. Отдельный WebSocket-сервер — вынесение Socket.io в самостоятельный микросервис

Подход 1: Пользовательский сервер с Socket.io

Установка зависимостей

npm install socket.io socket.io-client

Создание пользовательского сервера

Создайте файл server.js в корне проекта:

const { createServer } = require("http");
const { parse } = require("url");
const next = require("next");
const { Server } = require("socket.io");

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;

const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const httpServer = createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    handle(req, res, parsedUrl);
  });

  const io = new Server(httpServer, {
    cors: {
      origin: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
      methods: ["GET", "POST"],
    },
  });

  io.on("connection", (socket) => {
    console.log(`Клиент подключился: ${socket.id}`);

    socket.on("join-room", (roomId) => {
      socket.join(roomId);
      socket.to(roomId).emit("user-joined", { userId: socket.id });
    });

    socket.on("send-message", ({ roomId, message, author }) => {
      io.to(roomId).emit("new-message", {
        id: Date.now(),
        message,
        author,
        timestamp: new Date().toISOString(),
      });
    });

    socket.on("disconnect", () => {
      console.log(`Клиент отключился: ${socket.id}`);
    });
  });

  httpServer.listen(port, () => {
    console.log(`> Сервер запущен на http://${hostname}:${port}`);
  });
});

Обновление package.json

{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}

Важно: при использовании пользовательского сервера next dev больше не используется — запуск осуществляется через node server.js.

Подход 2: Отдельный WebSocket-сервер

Этот вариант лучше подходит для продакшена и микросервисной архитектуры.

Структура проекта

/
├── apps/
│   ├── web/          # Next.js приложение
│   └── ws-server/    # Отдельный WebSocket-сервер
└── package.json

WebSocket-сервер (apps/ws-server/index.js)

const { createServer } = require("http");
const { Server } = require("socket.io");

const httpServer = createServer();
const io = new Server(httpServer, {
  cors: {
    origin: process.env.WEB_APP_URL || "http://localhost:3000",
    methods: ["GET", "POST"],
  },
});

io.on("connection", (socket) => {
  socket.on("join-room", (roomId) => {
    socket.join(roomId);
  });

  socket.on("send-message", (data) => {
    io.to(data.roomId).emit("new-message", data);
  });
});

const WS_PORT = process.env.WS_PORT || 3001;
httpServer.listen(WS_PORT, () => {
  console.log(`WebSocket-сервер запущен на порту ${WS_PORT}`);
});

Клиентская часть в Next.js

Создание хука для Socket.io

Создайте файл hooks/use-socket.ts:

import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";

interface UseSocketOptions {
  url?: string;
}

export function useSocket(options: UseSocketOptions = {}) {
  const socketRef = useRef<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  const url =
    options.url ||
    process.env.NEXT_PUBLIC_SOCKET_URL ||
    "http://localhost:3000";

  useEffect(() => {
    const socket = io(url, {
      transports: ["websocket", "polling"],
    });

    socketRef.current = socket;

    socket.on("connect", () => setIsConnected(true));
    socket.on("disconnect", () => setIsConnected(false));

    return () => {
      socket.disconnect();
    };
  }, [url]);

  return { socket: socketRef.current, isConnected };
}

Компонент чата

"use client";

import { useEffect, useState, useCallback } from "react";
import { useSocket } from "@/hooks/use-socket";

interface Message {
  id: number;
  message: string;
  author: string;
  timestamp: string;
}

interface ChatProps {
  roomId: string;
  username: string;
}

export function Chat({ roomId, username }: ChatProps) {
  const { socket, isConnected } = useSocket();
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputValue, setInputValue] = useState("");

  useEffect(() => {
    if (!socket || !isConnected) return;

    socket.emit("join-room", roomId);

    socket.on("new-message", (message: Message) => {
      setMessages((prev) => [...prev, message]);
    });

    socket.on("user-joined", ({ userId }: { userId: string }) => {
      console.log(`Пользователь ${userId} присоединился к комнате`);
    });

    return () => {
      socket.off("new-message");
      socket.off("user-joined");
    };
  }, [socket, isConnected, roomId]);

  const sendMessage = useCallback(() => {
    if (!socket || !inputValue.trim()) return;

    socket.emit("send-message", {
      roomId,
      message: inputValue,
      author: username,
    });

    setInputValue("");
  }, [socket, inputValue, roomId, username]);

  return (
    <div className="chat-container">
      <div className="status">
        {isConnected ? "Подключено" : "Отключено"}
      </div>
      <div className="messages">
        {messages.map((msg) => (
          <div key={msg.id} className="message">
            <strong>{msg.author}:</strong> {msg.message}
            <span className="timestamp">
              {new Date(msg.timestamp).toLocaleTimeString()}
            </span>
          </div>
        ))}
      </div>
      <div className="input-area">
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && sendMessage()}
          placeholder="Введите сообщение..."
        />
        <button onClick={sendMessage}>Отправить</button>
      </div>
    </div>
  );
}

Пространства имён и комнаты

Socket.io поддерживает пространства имён (namespaces) для логического разделения соединений и комнаты (rooms) для группировки клиентов.

Пространства имён на сервере

const chatNamespace = io.of("/chat");
const notificationsNamespace = io.of("/notifications");

chatNamespace.on("connection", (socket) => {
  socket.on("send-message", (data) => {
    chatNamespace.to(data.roomId).emit("new-message", data);
  });
});

notificationsNamespace.on("connection", (socket) => {
  socket.on("subscribe", (userId) => {
    socket.join(`user:${userId}`);
  });
});

function sendNotification(userId, notification) {
  notificationsNamespace
    .to(`user:${userId}`)
    .emit("notification", notification);
}

Подключение к пространству имён на клиенте

import { io } from "socket.io-client";

const chatSocket = io("http://localhost:3000/chat");
const notificationsSocket = io("http://localhost:3000/notifications");

notificationsSocket.on("connect", () => {
  notificationsSocket.emit("subscribe", currentUserId);
});

notificationsSocket.on("notification", (notification) => {
  showToast(notification.message);
});

Аутентификация WebSocket-соединений

Для защищённых приложений необходимо проверять подлинность WebSocket-соединений.

Передача токена при подключении

// Клиент
const socket = io("http://localhost:3000", {
  auth: {
    token: localStorage.getItem("access-token"),
  },
});

Middleware для проверки токена на сервере

const jwt = require("jsonwebtoken");

io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  if (!token) {
    return next(new Error("Токен авторизации отсутствует"));
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    socket.data.user = decoded;
    next();
  } catch (err) {
    next(new Error("Недействительный токен"));
  }
});

io.on("connection", (socket) => {
  const { user } = socket.data;
  console.log(`Пользователь ${user.name} подключился`);
  socket.join(`user:${user.id}`);
});

Масштабирование с Redis Adapter

При развёртывании нескольких экземпляров сервера необходим общий брокер сообщений. Socket.io поддерживает Redis через официальный адаптер.

npm install @socket.io/redis-adapter ioredis
const { createAdapter } = require("@socket.io/redis-adapter");
const { createClient } = require("ioredis");

const pubClient = createClient({ host: "localhost", port: 6379 });
const subClient = pubClient.duplicate();

Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
  io.adapter(createAdapter(pubClient, subClient));
  httpServer.listen(port);
});

С Redis-адаптером события, отправленные на одном экземпляре сервера, автоматически доставляются клиентам, подключённым к другим экземплярам.

Мониторинг активных соединений

io.on("connection", async (socket) => {
  const sockets = await io.fetchSockets();
  console.log(`Активных соединений: ${sockets.length}`);

  socket.on("disconnect", async () => {
    const remaining = await io.fetchSockets();
    console.log(`Активных соединений: ${remaining.length}`);
  });
});

Особенности деплоя

Vercel и serverless-окружения

Vercel не поддерживает WebSockets в serverless-функциях — постоянные соединения несовместимы с моделью запрос/ответ. Для деплоя на Vercel WebSocket-сервер необходимо выносить в отдельный сервис (Railway, Render, Fly.io или собственный VPS).

# .env.local
NEXT_PUBLIC_SOCKET_URL=https://ws.your-domain.com

Nginx как прокси для WebSocket

server {
  listen 80;
  server_name ws.your-domain.com;

  location / {
    proxy_pass http://localhost:3001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

Заголовки Upgrade и Connection обязательны — без них Nginx не передаст WebSocket-соединение бэкенду.

Итог

WebSockets в Next.js требуют выхода за рамки стандартных Route Handlers. Оптимальная стратегия:

  • Для прототипов и небольших проектов — пользовательский сервер с Socket.io в том же процессе
  • Для продакшена и масштабируемых систем — отдельный WebSocket-сервис с Redis-адаптером
  • При хостинге на Vercel — обязательное вынесение WebSocket-сервера на отдельную инфраструктуру

Socket.io значительно упрощает работу с WebSockets благодаря встроенным механизмам переподключения, комнатам и фолбэку на polling при недоступности WebSocket.

Освоить Next.js с нуля до уровня production-разработчика можно на курсе Next.js на PurpleSchool.

Next.js с Stripe — интеграция платёжной системыСтрелочка вправо

Все гайды по Next-js

useSearchParams и usePathname в Next.js App RouterStreaming и Suspense в Next.jsServer Actions в Next.jsrevalidateTag и revalidatePath — инвалидация кэша в Next.jsPrisma ORM в Next.jsnotFound и redirect в серверных компонентах Next.jsNext.js с Supabase — авторизация и база данныхNext.js с GraphQL и Apollo ClientNext.js 15: новые возможности и breaking changesБлог и документация на Next.js с MDXloading.js и error.js в Next.js App RouterАутентификация с JWT в App Router без сторонних библиотекИнтернационализация i18n в Next.js App RoutergenerateStaticParams — статическая генерация динамических маршрутовNext.js cookies и headers в App RouterNext.js с Clerk: аутентификация пользователейNext.js after() — выполнение кода после отправки ответа
Открыть базу знаний

Отправить комментарий