53

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.

1

5 Answers 5

54

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>
}
Sign up to request clarification or add additional context in comments.

22 Comments

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.
I actually just switched everything to Pusher because Vercel says in their limitations docs that websockets are not supported in serverless functions. Also, they have a guide for how to set up pusher.
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.
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 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)
@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!
|
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.

6 Comments

So is this how we authenticate: var pusher = new Pusher({ appId: '', key: '', secret: '', cluster: 'eu', encrypted: true });
Hopefully this gist helps.
Thanks for the response - I got up to that point, but I had some issues with 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.
Did you post an issue in the repo? That's a question for the use-pusher author.
That's a good idea - I just did here: 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?
|
0

This helps me, first, go to cmd.

  1. rmdir /s /q <folder name>\.git

  2. git add <folder name>/

  3. git status -- If the output is something like:

    Changes to be committed: (use "git restore --staged ..." to unstage)

Proceed to:

  1. git commit -m "Add server folder"
  2. git push

Then done.

Comments

0

I don't know why everyone overcomplicates the code examples for client sockets with Next.js or mixes up the idea that you need to run a socket server inside of Next.js when you don't. Even the socket.io documentation for Next.js is really bad, their documentation just tells you to stop using the Next.js web server with all of its great features and use a different HTTP server (very bad idea). People are looking for how to stream incoming socket data into Next.js components which they should have documentation for.

Here's a super simple client example for Next.js, it assumes you have a socket server running somewhere else which is recommended. It also assumes you have CORS configured correclty on your socket server to allow incoming connections from the Next.js server that is interacting with it. Once you set up the API, you can call that programmatically and feed data to your components however you like.

This is for the Page Router and not the App router. ChatGPT can probably give you good suggestions on how to refactor it for the AppRouter.

// src/pages/api/socketStatus.js
import { io } from "socket.io-client";

export const config = {
    api: {
      externalResolver: true,
    },
  };

export default function socketHandler() {

// my socket server is running on port 5174 with CORS configured there allowing requests from my Next.js origin of http://localhost:3000
let clientSocket = io('http://localhost:5174');

clientSocket.on("connect", () => {
  console.log(clientSocket.id) // log the socket ID to the console.
    return console.log("Connected")
})

// log all incoming socket data to a variable of 'data' from the socket topic of 'importantSocketTopic`, change this to the string name that you emit data with.
 clientSocket.on("importantSocketTopic", data => {
     console.log(data);
} )

}

Calling the socket API route from your Next.js index.tsx file as an example:

// src/pages/index.tsx
import { useEffect, useState } from 'react'

export default function Home() {
  const [mounted, setMounted] = useState(false)

    useEffect(() => {
      initialize();

      setMounted(true);

    }, []);

  async function initialize() {
    // initialize any functions or services for the web dashboard

    let socket_check = await fetch('/api/socketStatus');
    console.log(socket_check);

  }


  if (!mounted) {
    return <>Ehhh we are waiting for something to render</>
  }


  return (
    <div>
<div>Your fancy nextjs app</div>
      </div>
    </div>
  )
}

Comments

-1

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

1 Comment

I think this is the correct fix but don't forget to set the defaultServer flag to true otherwise you won't be able to make a correct build.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.