DEV Community

Melle Wynia
Melle Wynia

Posted on • Edited on

Quick guide to add websocket to Nuxt 3

I found it quite challenging to add websockets to Nuxt 3. The docs on how Nuxt 3 is glued together with Nitro, h3 and other services cannot always be easy to follow and these are documented separately. Here's a guide.

Our use case is simple. Once we update a resource (via an Nuxt 3 ~/server/api/*.post.ts call), a message needs to be sent to all listeners of that resource. Simply put, when a user updates a project item, all other users see the updates as well. Immediately.

Good to know before you start: it is good to keep in mind that this implementation will (likely) not work in a serverless setup. Mine is hosted as one instance, at Digital Ocean apps platform. That works fine.

Receiving messages in backend

First make sure you install socket.io package.

Then add a new Nuxt middleware layer. This allows us to initiate the logic and then wire an object with methods which allows us to send messages in the Nuxt api (located ~/server/api) via the event.context.

Note: This is a simplified example with console.logs to help you see how it should work (used versions: Nuxt ^3.8.0 and socket.io@^4.7.2). Right now it does a global emit, make sure to add the appropriate auth before running in prod apps.

// ~/server/middleware/socket.middleware.ts

import { Socket, Server } from 'socket.io'

let appSocket = {
  emit: (channel: string, message: string) => {
    console.log('Not initiated yet', channel, message)
  }
}

export default defineEventHandler((event) => {
  event.context.appSocket = appSocket

  if (global.io) return
  console.log('Initiating socket.middleware')

  const node = event.node
  global.io = new Server(node.res.socket?.server)
  global.io.on('connection', (socket: Socket) => {
    socket.emit('message-channel', `welcome ${socket.id}`)

    listeners.push({ channel: 'message', socket })

    appSocket.emit = (channel, message) => {
      global.io.emit(channel, message)
    }

    socket.on('message', (data) => {
      console.log('Relaying again for funsies: ', data)
      global.io.emit('message-channel', 'Hello, client! ' + data)
    })

    socket.on('disconnect', () => {
      // Put optional disconnect logic here
    });
  })
})
Enter fullscreen mode Exit fullscreen mode

Sending messages from backend api

This snipplet contains all logic to connect websocket in the frontend and receive and send messages. Please note how we need to use the channel name 'message-channel'. Once you put this in your own app, replace it with more appropriate names resembling a resource, or actions on a resource.

// ~/server/api/test-message.post.ts

export default defineEventHandler(
  async (event) => {
    const body = await readBody(event)

    // Make sure to do this in a safe way
    event.context?.appSocket.emit('message-channel', body.value)
    return body.value
  }
)
Enter fullscreen mode Exit fullscreen mode

Receiving and sending messages in frontend

First step is to create a socket instance on β€˜onMounted’ and start listening to the channel message-channel. There are two ways to send messages. You can utilize the websocket or call an api method, a more traditional flow.

// ~/pages/test-nuxt3-websocket.vue

<script setup lang="ts">
import io from 'socket.io-client'
import type { Socket } from 'socket.io-client'

const formMessage = ref('there you go')
const state = reactive({messages: []})

let socket: Socket | undefined

const sendMessage = () => {
  socket?.emit('message-channel', formMessage.value)
}

const sendMessageViaApi = async () => {
  socket?.emit('message-channel', formMessage.value)
  await $fetch('/api/test-message', {
    method: 'POST',
    body: { message: formMessage.value },
  })
}

onMounted(async () => {
  socket = io(`${location.protocol === 'https:' ? 'wss://' : 'ws://' }${location.host}`)

  socket.on('message-channel', (messageValue: string) => {
    try {
      console.log('Frontend received: ', messageValue)
      state.messages.push(messageValue)
    } catch (e) { console.error(e) }
  })
})

onUnmounted(() => {
  socket?.disconnect()
})
</script>

<template>
  <div>
    <ul>
      <li v-for="message of state.messages" :key="message">
        {{ message}}
      </li>
    </ul>
    <form @submit.prevent="sendMessage">
      <input v-model="formMessage" />
      <button>Send message</button>
      <button type="button" @click="sendMessageViaApi()">Send via api</button>
    </form>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Done

Hopefully this guide gets you up to speed quickly for using Websockets in Nuxt 3. Best. - M

Top comments (3)

Collapse
 
insanity54 profile image
chris grimmett • Edited

Thank you for this guide! Unfortunately I'm having trouble following.

[nitro] [uncaughtException] ReferenceError: listeners is not defined

Is there an example code repo that I could see? Thanks again.

Collapse
 
shroomlife profile image
shroomlife πŸ„

Not working with TypeScript

event.node.res.socket?.server

  • Property 'server' does not exist on type 'Socket'.
Collapse
 
ikudev profile image
Edgar

Thanks a lot for sharing such a helpful post. It would be better to make it a nuxt module?