48

Next.js provides serverless API routes. By creating a file under ./pages/api you can have your service running, and I want to have a Socket.io service by using this mechanism.

I have created a client:

./pages/client.js

import { useEffect } from 'react';
import io from 'socket.io-client';

export default () => {

  useEffect(() => {
    io('http://localhost:3000', { path: '/api/filename' });
  }, []);

  return <h1>Socket.io</h1>;
}

And an API route:

./pages/api/filename.js

const io = require('socket.io')({ path: '/api/filename' });

io.onconnection = () => {
  console.log('onconnection');
}

io.on('connect', () => {
  console.log('connect');
})

io.on('connection', () => {
  console.log('connection');
})

export default (req, res) => {
  console.log('endpoint');
}

But I can't get the client to connect to the Socket.io server and succesfully see any of: 'onconnection', 'connect', or 'connection' printed.

Aurelius
  • 689
  • 1
  • 6
  • 16

3 Answers3

52

The trick is to plug 'socket.io' into the http server only once, so checking every access to the api. Try something like this:

./pages/api/socketio.js

import { Server } from 'socket.io'

const ioHandler = (req, res) => {
  if (!res.socket.server.io) {
    console.log('*First use, starting socket.io')

    const io = new Server(res.socket.server)

    io.on('connection', socket => {
      socket.broadcast.emit('a user connected')
      socket.on('hello', msg => {
        socket.emit('hello', 'world!')
      })
    })

    res.socket.server.io = io
  } else {
    console.log('socket.io already running')
  }
  res.end()
}

export const config = {
  api: {
    bodyParser: false
  }
}

export default ioHandler

./pages/socketio.jsx

import { useEffect } from 'react'
import io from 'socket.io-client'

export default () => {
  useEffect(() => {
    fetch('/api/socketio').finally(() => {
      const socket = io()

      socket.on('connect', () => {
        console.log('connect')
        socket.emit('hello')
      })

      socket.on('hello', data => {
        console.log('hello', data)
      })

      socket.on('a user connected', () => {
        console.log('a user connected')
      })

      socket.on('disconnect', () => {
        console.log('disconnect')
      })
    })
  }, []) // Added [] as useEffect filter so it will be executed only once, when component is mounted

  return <h1>Socket.io</h1>
}
Ajith Gopi
  • 1,509
  • 1
  • 12
  • 20
rogeriojlle
  • 1,046
  • 1
  • 11
  • 19
  • 1
    How can this be used with external applications? Say I have a mobile app that's connecting to this API and want to listen for document change inside models. For example, Mongoose `post('save')`. – francis Jul 11 '20 at 15:10
  • 9
    One problem with this solution in development is that after `res.socket.server.io` is set, you can't take advantage of hot-reloading for code inside that if statement. – Daniel Jul 25 '20 at 05:34
  • @Daniel have you found a way around this? – Alex Cory Sep 08 '20 at 00:54
  • @AlexCory unfortunately I have not – Daniel Sep 09 '20 at 03:38
  • 11
    I actually just switched everything to Pusher because Vercel says in their [limitations docs](https://vercel.com/docs/platform/limits#websockets) that websockets are not supported in serverless functions. Also, they have a [guide](https://vercel.com/guides/deploying-pusher-channels-with-vercel) for how to set up pusher. – Alex Cory Sep 09 '20 at 15:35
  • Yes, there are limitations when the application is hosted on Vercel, such as, for example, the maximum time for each execution, forcing the websocket to end. But I assumed that the local structure is being used, since the url in the question is defined as 'localhost: 3000', in this situation the suggestion works well. – rogeriojlle Sep 09 '20 at 16:14
  • @AlexCory but Pusher uses Websockets right, so how would that work out? Surely the same outcome? – traderjosh Sep 14 '20 at 14:33
  • 3
    Yes pusher does use websockets, however when interacting with pusher you are only making REST API calls. POST to be specific. All the websocket stuff is handled clientside between your app and pusher. – Alex Cory Sep 14 '20 at 21:48
  • @AlexCory excellent solution, have you got a source to learn more about this? Or just the Pusher docs? Also, doesn't that open up security risks if the networking is handled client side? – traderjosh Sep 14 '20 at 22:32
  • 2
    1. you setup a serverless function that authenticates with pusher `/api/pusher/auth` 2. then your frontend uses the key from that to authenticate your frontend with pusher 3. I used [use-pusher](https://github.com/mayteio/use-pusher) react hooks for listening for events on the frontend. Specifically `useEvent` and the `Provider` 4. I created endpoints such as a create-message endpoint, then after creating the message + saving to db, I used `pusher.trigger('private-channel-name', 'message:created', message)` – Alex Cory Sep 14 '20 at 22:45
  • @AlexCory why do you need `/api/pusher/auth` though if you've got the `Provider` that does the connecting? You just wrap your app with the provider and create channels/events in your components right? – traderjosh Sep 16 '20 at 12:08
  • @rogeriojlle. Thank you alot for posting your solution on how to handle socketio with nextjs. This is what I needed. I am facing one problem though. One instance of a socket connection creates two instances on the server. can you help me out – George Jan 31 '21 at 20:39
  • @George. Is it not possible that if you do the first with a browser it, for whatever reason, makes more than one request very quickly, not giving the server time to check if the instance already exists? What if you request the url as soon as you run the server-side script before trying to use the browser? – rogeriojlle Feb 01 '21 at 23:11
  • @rogeriojlle Yes you are right. I found i was making 2 instances of Websocket that's why there were two requests happening. I was wondering why most comments here are talking about Pusher but your answer seems very right and mostly applicable and I like it. Is there any cons of using your provided solution? – George Feb 03 '21 at 09:48
  • @George In my opinion, Next Js was made with the intention of being used in Vercel's structure, and once there you will have to deal with the limitations already mentioned, so in the case of Socket.io you need to do something when your script is shot down by the execution timeout, and that’s where the Pusher comes in. For this reason I prefer to use Meteor . But let's be honest here, Meteor also has some details when using websockets. – rogeriojlle Feb 04 '21 at 13:21
  • 2
    @rogeriojlle I'm trying to understand what this res.socket.server property is. I'm using Typescript and it complains that server doesn't exist on the socket property. I've tried using the NextApiResponse type, but still the same error. Is there any documentation about this socket and server property? Thanks! – Gabriel G. Roy Mar 26 '22 at 01:22
  • 1
    It is just the native http server from which socketio was "attached". And Typescript is called punishment, you spend 99% of your time trying to explain to him what you're doing instead of coming up with something logical. Maybe this help: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node – rogeriojlle Mar 28 '22 at 11:57
  • 1
    @GabrielG.Roy It's an undocumented implementation detail of Node.js: https://github.com/nodejs/node/blob/6f924ac6914b3e09c992a2b7b4cd5adb9eb7aa96/lib/_http_server.js#L542. Luckily according to the comment above, it shouldn't be changed in future. For using with TypeScript, `as any` is required here. – yume_chan Jun 23 '22 at 08:03
  • you could take the example and host it on heroku since they provide websockets. https://devcenter.heroku.com/articles/node-websockets – Marcel Dz Jul 18 '22 at 13:17
  • I face the issue where the resource is not found when connecting to the socket when deployed to Vercel. – Haneen Mahdin Dec 30 '22 at 11:39
  • 2
    I get a 404 error, meaning /socket.io does not exist, when the server is started, but only in my newest project, which is using a newer version of NextJS. It is incredibly frustrating. – france1 May 01 '23 at 09:23
  • the socket.io is not going to work, the req.socket.server.io is null and i cant do anything with it – Samyar Jun 20 '23 at 17:35
0

You have to have the /api/pusher/auth to authenticate with pusher on the frontend. Then you use the key you get from that to communicate with pusher. It's for security purposes. You can do it all through the frontend, but depending on your app, if you're saving data (such as messages, or chats) then probably should authenticate.

Alex Cory
  • 10,635
  • 10
  • 52
  • 62
  • 1
    So is this how we authenticate: `var pusher = new Pusher({ appId: '', key: '', secret: '', cluster: 'eu', encrypted: true });` – traderjosh Sep 22 '20 at 17:36
  • Hopefully [this gist](https://gist.github.com/alex-cory/84451eba3257953ca5ccd979c00f9962) helps. – Alex Cory Sep 23 '20 at 00:19
  • Thanks for the response - I got up to that point, but I had some issues with [use-pusher](https://github.com/mayteio/use-pusher), particularly the `usePresenceChannel` because I wanted to track my users and display them in the chatroom, but presence channel doesn't work sadly. Have you tried that? It was throwing some warnings for no auth headers with the presenceChannel and not registering the trigger events. I didn't find this problem with the normal `channel` like your gist. – traderjosh Sep 23 '20 at 20:16
  • Did you post an issue in the repo? That's a question for the use-pusher author. – Alex Cory Sep 23 '20 at 21:05
  • That's a good idea - I just did here: https://github.com/mayteio/use-pusher/issues/26. I was thinking of writing writing my own implementation to avoid these kind of issues before you recommended the library. It seems well-written so maybe I'm doing something wrong. Have you tried the presence channel? How else would you track your chatroom users? – traderjosh Sep 25 '20 at 20:01
  • Why not just create a `chat` table in your db where each chat would look like `{ chatId: 'id', members: ['creator-user-id', 'member-1-user-id', 'etc.'], creatorId: 'creator-user-id' }` – Alex Cory Sep 25 '20 at 23:56
-2

You can use custom server and attach sockets to it (just like with express) and provide needed path where socket.io will listen. How to use custom server

You can write something like this 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;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

app.prepare().then(() => {
    const server = createServer(async (req, res) => {
        try {
            // Be sure to pass `true` as the second argument to `url.parse`.
            // This tells it to parse the query portion of the URL.
            const parsedUrl = parse(req.url, true);
            const { pathname, query } = parsedUrl;

            if (pathname === '/a') {
                await app.render(req, res, '/a', query);
            } else if (pathname === '/b') {
                await app.render(req, res, '/b', query);
            } else {
                await handle(req, res, parsedUrl);
            }
        } catch (err) {
            console.error('Error occurred handling', req.url, err);
            res.statusCode = 500;
            res.end('internal server error');
        }
    });

    const io = new Server(server, {
        path: '/socket.io' // or any other path you need
    });

    io.on('connection', socket => {
        // your sockets here
        console.log('IO_CONNECTION');
    });

    server.listen(port, err => {
        if (err) throw err;
        console.log(`> Ready on http://${hostname}:${port}`);
    });
});

You would need to run your server using node server.js