Post

Guide Create Chat Socket.io SSR Nuxt app

Harianto van Insulinde

Boilerplates for Express and Fastify.

ExpressJS

Understanding the basic way for enabling socket.io in Nuxt Express app and use Vue.observable as global store.

Generate Nuxt Express app from command

bash
npx create-nuxt-app chat-express-nuxt

Output:

bash
create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in chat-express-nuxt
? Project name chat-express-nuxt
? Project description Chat Express Nuxt
? Author name Harianto
? Choose programming language JavaScript
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework Express
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support
? Choose linting tools Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools jsconfig.json (Recommended for VS Code)

- chat-express-nuxt
- Chat Express Nuxt
- Harianto
- JavaScript
- npm
- None
- Express
- Axios, PWA
- Prettier
- None
- SSR
- jsconfig.json

Optional

bash
cd chat-express-nuxt

# Save Node version to a file
node -v > .nvmrc

# optional
git add .
git commit -avm "INIT Express PWA SSR Nuxt - Node $(node -v) | NPM $(npm -v)"

Always save node & npm versions in your projects.

Socket.io

Add socket.io from NPM and SASS

bash
npm i -S socket.io axios
npm i -D node-sass sass-loader

Apply socket.io

We require http and use that to create server instead of express from the default generated project.
I highlight the lines of code in the snippet below:

js
const http = require('http'),
  express = require('express'),
  app = express(),
  { Router } = express,
  server = http.createServer(app),
  io = require('socket.io')(server)

async function start() {
  
  app.use(express.json()) // Receive Header: Content-Type: application/json
  app.use(express.urlencoded({ extended: false })) // Parse Body to JSON
  app.use(nuxt.render)

  // Listen the server
  server.listen(port, host)
}

Snippet: server/index.js

Becomes:

js
const http = require('http'),
  express = require('express'),
  app = express(),
  { Router } = express,
  server = http.createServer(app),
  io = require('socket.io')(server)
const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

async function start() {
  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  const { host, port } = nuxt.options.server

  await nuxt.ready()
  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  }

  app.use(express.json())
  app.use(express.urlencoded({ extended: false }))
  // // Give nuxt middleware to express
  app.use(nuxt.render)

  // Listen the server
  server.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()

File: server/index.js

Start the server and check socket.io

bash
npm run dev

Integrating socket.io

Router: message

js
module.exports = ({ Router, io }) => {
  const router = Router()
  /* GET users listing. */
  router
    .get('/', function(req, res, next) {
      res.json({
        message: true
      })
    })
    .post('/', (req, res) => {
      io.emit('chat-message', { ...req.body, id: 'POST' })
      res.json(req.body)
    })

  return router
}

File: api/router/message.js

API: index

js
module.exports = ({ Router, io }) => {
  const router = Router()
  /* GET users listing. */
  router
    .get('/', function(req, res, next) {
      res.json({
        api: true
      })
    })
    // Router: message
    .use('/message', require('./router/message')({ Router, io }))

  io.on('connection', socket => {
    console.log('Socket Connect:', { id: socket.id })
    io.emit('chat-message', { id: socket.id, status: 'ENTERS ROOM' })
    socket.on('disconnect', message => {
      console.log({ id: socket.id, message })
      socket.broadcast.emit('chat-message', { id: socket.id, status: 'LEFT' })
    })

    socket.on('chat-message', msg => {
      console.log('chat-message:', msg)
      io.emit('chat-message', { id: socket.id, message: msg })
    })
  })

  return router
}

File: api/index.js

Connect Express api Router

js
app.use('/api', require('../api')({ Router, io }))

Snippet: server/index.js

Becomes:

js
const http = require('http'),
  express = require('express'),
  app = express(),
  { Router } = express,
  server = http.createServer(app),
  io = require('socket.io')(server)

const consola = require('consola')
const { Nuxt, Builder } = require('nuxt')

// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

async function start() {
  // Init Nuxt.js
  const nuxt = new Nuxt(config)

  const { host, port } = nuxt.options.server

  await nuxt.ready()
  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  }

  app.use(express.json())
  app.use(express.urlencoded({ extended: false }))
  app.use('/api', require('../api')({ Router, io }))
  // Give nuxt middleware to express
  app.use(nuxt.render)

  // Listen the server
  server.listen(port, host)
  consola.ready({
    message: `Server listening on http://${host}:${port}`,
    badge: true
  })
}
start()

File: server/index.js

Add local script

js
export default {
    head() {
        return {
            script: [{ src: '/socket.io/socket.io.js' }]
        }
    }
}

Snippet: pages/index.vue

html
<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">chat-express-nuxt</h1>
      <h2 class="subtitle">Chat Express Nuxt</h2>
      <div class="links">
        <a href="https://nuxtjs.org/" target="_blank" class="button--green">Documentation</a>
        <a href="https://github.com/nuxt/nuxt.js" target="_blank" class="button--grey">GitHub</a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'

export default {
    head() {
        return {
            script: [{ src: '/socket.io/socket.io.js' }]
        }
    },
    components: {
        Logo
    }
}
</script>

<style>
.container {
    margin: 0 auto;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
}

.title {
    font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
        'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    display: block;
    font-weight: 300;
    font-size: 100px;
    color: #35495e;
    letter-spacing: 1px;
}

.subtitle {
    font-weight: 300;
    font-size: 42px;
    color: #526488;
    word-spacing: 5px;
    padding-bottom: 15px;
}

.links {
    padding-top: 15px;
}
</style>

File: pages/index.vue

Alternative global settings

js
export default {
  head: {
    script: [
      { src: '/socket.io/socket.io.js' }
    ]
  }
}

Snippet: nuxt.config.js

Page Layout

Store: messages

js
import Vue from 'vue'

const state = Vue.observable({
    messages: []
  }),
  methods = {
    onMessage(message) {
      state.messages.push(message)
    }
  }

export { state, methods }

File: lib/store/storeMessages.js

Page

html
<template>
  <div class="app-tpl">
    <h1>{{title}}</h1>
    <input v-model="formData.message" placeholder="Message" />
    <div>
      <button @click="onEmit">Emit Message</button>
      <button @click="onPost">Post Message</button>
    </div>
    <ul class="messages">
      <li v-for="msg in messages">
        <pre v-text="msg" />
      </li>
    </ul>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'
import { state, methods } from '~/lib/store/storeMessages'
import axios from 'axios'
const post = (url, data) => axios.post(url, data).then(({ data }) => data)

let socket

export default {
    head() {
        return {
            script: [{ src: '/socket.io/socket.io.js' }]
        }
    },
    components: {
        Logo
    },
    data: () => ({
        title: 'Chat Nuxt App',
        formData: {
            message: ''
        }
    }),
    computed: {
        messages() {
            return state.messages
        }
    },
    methods: {
        onEmit() {
            const { formData } = this
            socket.emit('chat-message', formData.message)
            console.log('onEmit:', formData.message, socket.id)
            formData.message = ''
        },
        onPost() {
            const { formData } = this,
                url = '/api/message'
            console.log('onPost:', formData.message)
            post(url, {
                id: socket.id,
                message: formData.message
            })
                .then(console.log.bind(console, 'RESPONSE:'))
                .catch(console.error.bind(console, 'FAIL - onPost:'))
            formData.message = ''
        }
    },
    // Life Cycle Hooks
    mounted() {
        socket = io()
        socket.on('chat-message', methods.onMessage)
    }
}
</script>

<style lang="scss">
body {
    margin: 0;
    padding: 0;
}

.app-tpl {
    height: calc(100vh);
    overflow-y: auto;
    overflow-x: hidden;
    background-color: rgb(248, 239, 201);
}

.messages {
    list-style: none;
    margin: 0;
    padding: 0;

    li {
        &:nth-child(odd) {
            background-color: yellow;
        }
    }
}
</style>

File: pages/index.vue

FastifyJS

Generate Nuxt Fastify app from command

bash
npx create-nuxt-app chat-fastify-nuxt

Output:

bash
create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in chat-fastify-nuxt
? Project name chat-fastify-nuxt
? Project description Chat Fastify Nuxt
? Author name Harianto
? Choose programming language JavaScript
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework Fastify
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support
? Choose linting tools Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools jsconfig.json (Recommended for VS Code)

- chat-fastify-nuxt
- Chat Fastify Nuxt
- Harianto
- JavaScript
- Npm
- None
- Fastify
- Axios, WPA
- Prettier
- None
- SSR
- jsconfig.json

Optional

bash
cd chat-fastify-nuxt

# Save Node version to a file
node -v > .nvmrc

# optional
git add .
git commit -avm "INIT Fastify PWA SSR Nuxt - Node $(node -v) | NPM $(npm -v)"

Socket.io

Add socket.io from NPM and SASS

bash
npm i -S socket.io axios
npm i -D node-sass sass-loader

Apply socket.io

We can use fastify.server to connect our socket.io.
Like so:

js
const io = require('socket.io')(fastify.server)

Snippet: server/index.js

js
const { Nuxt, Builder } = require('nuxt')
const fastify = require('fastify')({
    logger: true
  }),
  io = require('socket.io')(fastify.server)
// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

async function start() {
  // Instantiate nuxt.js
  const nuxt = new Nuxt(config)

  const {
    host = process.env.HOST || '127.0.0.1',
    port = process.env.PORT || 3000
  } = nuxt.options.server

  await nuxt.ready()
  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  }

  fastify.use(nuxt.render)

  fastify.listen(port, host, (err, address) => {
    if (err) {
      fastify.log.error(err)
      process.exit(1)
    }
  })
}

start()

File: server/index.js

Start the server and check socket.io

bash
npm run dev

Integrating socket.io

Router: message

js
module.exports = io => async (fastify, options) => {
  fastify
    // Route: /
    .get('/', async () => ({ message: true }))
    .post('/', {}, async (request, reply) => {
      io.emit('chat-message', {
        ...request.body,
        id: `POST: ${request.body.id}`
      })
      return request.body
    })
}

File: api/routes/message.js

Api: index

js
module.exports = io => async (fastify, options) => {
  fastify
    .get('/', async () => ({ api: true }))
    // register
    .register(require('./routes/message')(io), { prefix: '/message' })

  io.on('connection', socket => {
    console.log('Socket Connect:', { id: socket.id })
    io.emit('chat-message', { id: socket.id, status: 'ENTERS ROOM' })
    socket.on('disconnect', message => {
      console.log({ id: socket.id, message })
      socket.broadcast.emit('chat-message', { id: socket.id, status: 'LEFT' })
    })

    socket.on('chat-message', msg => {
      console.log('chat-message:', msg)
      io.emit('chat-message', { id: socket.id, message: msg })
    })
  })
}

File: api/index.js

Connect fastify api Router

js
fastify.register(require('../api')(io), { prefix: '/api' })
fastify.setNotFoundHandler(({ req }, { res }) => nuxt.render(req, res))

Snippet: server/index.js

Replace: fastify.use(nuxt.render)

js
const { Nuxt, Builder } = require('nuxt')
const fastify = require('fastify')({
    logger: true
  }),
  io = require('socket.io')(fastify.server)
// Import and Set Nuxt.js options
const config = require('../nuxt.config.js')
config.dev = process.env.NODE_ENV !== 'production'

async function start() {
  // Instantiate nuxt.js
  const nuxt = new Nuxt(config)

  const {
    host = process.env.HOST || '127.0.0.1',
    port = process.env.PORT || 3000
  } = nuxt.options.server

  await nuxt.ready()
  // Build only in dev mode
  if (config.dev) {
    const builder = new Builder(nuxt)
    await builder.build()
  }

  fastify.register(require('../api')(io), { prefix: '/api' })
  fastify.setNotFoundHandler(({ req }, { res }) => nuxt.render(req, res))

  fastify.listen(port, host, (err, address) => {
    if (err) {
      fastify.log.error(err)
      process.exit(1)
    }
  })
}

start()

File: server/index.js

Add local script

js
export default {
    head() {
        return {
            script: [{ src: '/socket.io/socket.io.js' }]
        }
    }
}

Snippet: pages/index.vue

html
<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">chat-nuxt</h1>
      <h2 class="subtitle">Chat Nuxt</h2>
      <div class="links">
        <a href="https://nuxtjs.org/" target="_blank" class="button--green">Documentation</a>
        <a href="https://github.com/nuxt/nuxt.js" target="_blank" class="button--grey">GitHub</a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'

export default {
    head() {
        return {
            script: [{ src: '/socket.io/socket.io.js' }]
        }
    },
    components: {
        Logo
    }
}
</script>

<style>
.container {
    margin: 0 auto;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
}

.title {
    font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
        'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    display: block;
    font-weight: 300;
    font-size: 100px;
    color: #35495e;
    letter-spacing: 1px;
}

.subtitle {
    font-weight: 300;
    font-size: 42px;
    color: #526488;
    word-spacing: 5px;
    padding-bottom: 15px;
}

.links {
    padding-top: 15px;
}
</style>

File: pages/index.vue

Alternative global settings

js
export default {
  head: {
    script: [
      { src: '/socket.io/socket.io.js' }
    ]
  }
}

Snippet: nuxt.config.js

Page Layout

Store: messages

js
import Vue from 'vue'

const state = Vue.observable({
    messages: []
  }),
  methods = {
    onMessage(message) {
      state.messages.push(message)
    }
  }

export { state, methods }

File: lib/store/storeMessages.js

Page

html
<template>
  <div class="app-tpl">
    <h1>{{title}}</h1>
    <input v-model="formData.message" placeholder="Message" />
    <div>
      <button @click="onEmit">Emit Message</button>
      <button @click="onPost">Post Message</button>
    </div>
    <ul class="messages">
      <li v-for="msg in messages">
        <pre v-text="msg" />
      </li>
    </ul>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'
import { state, methods } from '~/lib/store/storeMessages'
import axios from 'axios'
const post = (url, data) => axios.post(url, data).then(({ data }) => data)

let socket

export default {
    head() {
        return {
            script: [{ src: '/socket.io/socket.io.js' }]
        }
    },
    components: {
        Logo
    },
    data: () => ({
        title: 'Chat Nuxt App',
        formData: {
            message: ''
        }
    }),
    computed: {
        messages() {
            return state.messages
        }
    },
    methods: {
        onEmit() {
            const { formData } = this
            socket.emit('chat-message', formData.message)
            console.log('onEmit:', formData.message, socket.id)
            formData.message = ''
        },
        onPost() {
            const { formData } = this,
                url = '/api/message'
            console.log('onPost:', formData.message)
            post(url, {
                id: socket.id,
                message: formData.message
            })
                .then(console.log.bind(console, 'RESPONSE:'))
                .catch(console.error.bind(console, 'FAIL - onPost:'))
            formData.message = ''
        }
    },
    // Life Cycle Hooks
    mounted() {
        socket = io()
        socket.on('chat-message', methods.onMessage)
    }
}
</script>

<style lang="scss">
body {
    margin: 0;
    padding: 0;
}

.app-tpl {
    height: calc(100vh);
    overflow-y: auto;
    overflow-x: hidden;
    background-color: rgb(248, 239, 201);
}

.messages {
    list-style: none;
    margin: 0;
    padding: 0;

    li {
        &:nth-child(odd) {
            background-color: yellow;
        }
    }
}
</style>

File: pages/index.vue

Next

Other Nuxt SSR configurations

Babel sourceType

Make CommonJS and ES modules a love couple.

js
module.exports = {
  build: {
    babel: {
      sourceType: 'unambiguous'
    }
  }
}

Snippet: nuxt.config.js