Guide Create Chat Socket.io SSR Nuxt app

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

npx create-nuxt-app chat-express-nuxt

Output:

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

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

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:

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:

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

npm run dev

Integrating socket.io

Router: message

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

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

app.use('/api', require('../api')({ Router, io }))
Snippet: server/index.js

Becomes:

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

export default {
	head() {
		return {
			script: [{ src: '/socket.io/socket.io.js' }]
		}
	}
}
Snippet: pages/index.vue
<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

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

Page Layout

Store: messages

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

<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

npx create-nuxt-app chat-fastify-nuxt

Output:

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

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

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:

const io = require('socket.io')(fastify.server)
Snippet: server/index.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

npm run dev

Integrating socket.io

Router: message

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

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

fastify.register(require('../api')(io), { prefix: '/api' })
fastify.setNotFoundHandler(({ req }, { res }) => nuxt.render(req, res))
Snippet: server/index.js
Replace: fastify.use(nuxt.render)
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

export default {
	head() {
		return {
			script: [{ src: '/socket.io/socket.io.js' }]
		}
	}
}
Snippet: pages/index.vue
<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

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

Page Layout

Store: messages

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

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

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