Quick guide to add websocket to Nuxt 3
Tags: vue, nuxt
Published: 2023-11-18 08:16:43

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. 

<small>**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.</small>

```js
// ~/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
    });
  })
})
```

### 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.

```ts
// ~/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
  }
)
```

## 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.

```vue
// ~/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>
```

## Done

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