TL;DR
Интеграция Socket.io с Next.js 15 в одном процессе через кастомный server.js — рабочий паттерн для real-time приложений, жертвующий Vercel-деплоем ради единой сессии и упрощённой инфраструктуры. Разберём плюсы, подводные камни и альтернативы.
Введение: контекст проблемы
С выходом Next.js 15 многие разработчики столкнулись с ограничением: middleware не предоставляет хуков для WebSocket-подключений. Классическое решение с отдельным сервером для сокетов усложняет:
- аутентификацию (раздельные сессии)
- деплой (дополнительные инстансы)
- локальную разработку
Пример из практики: Minipad реализует альтернативный подход — единый Node.js процесс, где Next.js и Socket.io работают вместе.
Основная часть: кастомный сервер в Next.js 15
Архитектурный паттерн
// 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 app = next({ dev })
const handler = app.getRequestHandler()
app.prepare().then(() => {
const httpServer = createServer((req, res) => {
const parsedUrl = parse(req.url, true)
handler(req, res, parsedUrl)
})
const io = new Server(httpServer, {
path: '/api/socket',
cors: { origin: '*' } // На продакшене заменить на конкретные домены
})
io.on('connection', (socket) => {
console.log('Client connected:', socket.id)
socket.on('join-note', (noteId) => {
socket.join(`note:${noteId}`)
})
socket.on('update-content', ({ noteId, content }) => {
io.to(`note:${noteId}`).emit('content-updated', content)
})
})
httpServer.listen(3000, () => {
console.log('> Ready on http://localhost:3000')
})
})
Ключевые особенности:
- Общий HTTP-сервер: Next.js и Socket.io используют один экземпляр
http.Server - Единая сессия: Куки аутентификации доступны и в HTTP, и в WebSocket-запросах
- Порт 3000: Обрабатывает оба типа соединений
Trade-off анализатор
| Плюсы (+) | Минусы (-) |
|---|---|
| Упрощённый деплой | Потеря Vercel-деплоя |
| Общий контекст аутентификации | Нет автоматического масштабирования |
| Локальная разработка “как на проде” | Требует Node.js-сервер |
Практическое применение: real-time редактор
Интеграция с React
// hooks/useSocket.ts
import { useEffect, useState } from 'react'
import { io, Socket } from 'socket.io-client'
export const useSocket = (noteId: string) => {
const [socket, setSocket] = useState<Socket | null>(null)
const [content, setContent] = useState('')
useEffect(() => {
const socketInstance = io('', { path: '/api/socket' })
socketInstance.on('connect', () => {
socketInstance.emit('join-note', noteId)
})
socketInstance.on('content-updated', (newContent: string) => {
setContent(newContent)
})
setSocket(socketInstance)
return () => {
socketInstance.disconnect()
}
}, [noteId])
const emitUpdate = (newContent: string) => {
socket?.emit('update-content', { noteId, content: newContent })
}
return { content, emitUpdate }
}
Типичные проблемы и решения:
-
Гидратация в React 19:
- Проблема: Разрыв между SSR-контентом и клиентским состоянием
- Решение: Использовать
useSyncExternalStoreдля WebSocket-данных
-
Авторизация:
// В middleware Next.js io.use((socket, next) => { const cookie = socket.handshake.headers.cookie const session = parseCookie(cookie) if (!session.user) return next(new Error('Unauthorized')) socket.data.user = session.user next() })
Альтернативные подходы
1. Vercel-совместимый вариант (Edge Functions + отдельный сервер)
// app/api/socket/route.js
import { NextResponse } from 'next/server'
export const runtime = 'edge'
export async function GET() {
const response = NextResponse.next()
response.headers.set('upgrade', 'websocket')
response.headers.set('connection', 'upgrade')
return response
}
2. Server-Sent Events (SSE) как lightweight-альтернатива
// app/api/sse/route.ts
export async function GET() {
const stream = new TransformStream()
const writer = stream.writable.getWriter()
const encoder = new TextEncoder()
const send = (data: string) => {
writer.write(encoder.encode(`data: ${data}\n\n`))
}
// Пример подписки на изменения
setInterval(() => {
send(JSON.stringify({ time: Date.now() }))
}, 1000)
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
})
}
Заключение: когда что выбирать
Кастомный server.js подходит когда:
- Нужен полный контроль над сервером
- Real-time функциональность — ключевая
- Готовы поддерживать собственный сервер
Vercel + отдельный сервер лучше для:
- Быстрого старта
- Масштабируемости “из коробки”
- Проектов с преимущественно CRUD-логикой
Для большинства production-проектов с real-time требованиями паттерн единого процесса доказал свою эффективность, несмотря на потерю Vercel-специфичных фич. Главное — явно документировать архитектурные решения для команды.
Попробуй сам: DigitalOcean — $200 кредитов для новых пользователей.
Источник: https://www.reddit.com/r/reactjs/comments/1sz6ipl/built_a_selfhostable_notepad_with_nextjs_15/