Let's Build #001: Realtime Messenger App

06/21/2023
Project Tutorial

What is a web socket?

A WebSocket is a communication protocol that provides full-duplex communication channels over a single TCP connection. It enables interactive communication between a client (such as a web browser) and a server, allowing real-time data transfer and bi-directional communication.

Unlike traditional HTTP connections that follow a request-response model, where the client sends a request and the server responds, WebSocket allows both the client and server to send data at any time without the need for a new HTTP request.

WebSockets use a standardized protocol that starts with an HTTP handshake, and once established, the connection is upgraded to the WebSocket protocol. The data is then exchanged in frames, allowing for efficient and structured communication.

WebSockets have become popular for developing real-time web applications that require instant data updates and interactivity. They provide a reliable and efficient means of communication between clients and servers, enabling developers to build dynamic and responsive web applications.

I've been following Socket.IO for a while and wanted to give it a try. So what better way to build a real-time messenger application using our favourite frontend framework, Vue? You can check out the github repository for the code here: https://github.com/mrohner94/lets-build-001-websockets

This project provides the perfect opportunity to work a bit with some popular frontend frameworks such as Vue and Vuetify, as well as some popular backend technologies such as Node.js and Socket.IO

I will be using version 18.13.0 of node for this project. There are many ways that we could go about this project - but for sake of learning this as a fullstack application, we will be treating the frontend and backend of this as two separate applications.

Setup the backend

To get started, lets create a directory and initialize the backend.
              
mkdir messenger-app
cd messenger-app
mkdir backend
cd backend
npm init
            
            

From here, you can just hit enter to get through the CLI options. Next, we will install some dependencies.

              
npm i socket.io
npm i -D @types/node nodemon ts-node typescript
            
            

Next, let's add create a file called `tsconfig.json` in backend.

              
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}
            
            

And in backend, lets create a folder called 'src'. In 'src', let's create a file called 'index.ts'. index.ts will be the entry point to the backend application, our server.

              
import http from "http";
const server = http.createServer();
server.listen(3000, () => {
  console.log("listening on *:3000");
});
            
            

Lastly, to get this app running, we must update the scripts package.json to include this script:

              
"scripts": {
  "dev": "nodemon src/index.ts"
}
            
            

Now the initial set up of our node application is complete. If you navigate to backend/ in your terminal, you can run npm run dev and you will see that your server is listening on port 3000!

Setup the frontend

Next, we will build out the frontend. If you are only interested in learning the backend of this project you, you can skip ahead here. Otherwise, in your terminal navigate to the root of your project and run the following commands:

              
npm init vue@latest
            
            

For the project name, you can call it frontend. Select yes for typescript, vue router and pinia.
Next, we will install the dependencies. I will explain what they all do as we go along.

              
cd frontend
npm i uuid socket.io-client Vuetify
npm i -D @types/uuid
            
            

First, we are just going to add two interfaces to model some objects. In src, let's add a folder called types. In types, create the following file:

              
export interface AppState {
  userId: string;
  hex: string;
}

export interface AllMessage {
  userId: string;
  hex: string;
  msg: string;
}
              
            

Next, we will add some state variables. We can access these anywhere in our application.
We are using pinia - if you want to learn more about pinia, I highly suggest reading through their documentation: https://pinia.vuejs.org/

Navigate to the stores directory. Delete everything in it. Let's add two new files:

              
import type { AppState } from "@/types";
import { defineStore } from "pinia";

export const useAppState = defineStore("app", {
  state: (): AppState => ({
    userId: "",
    hex: "",
  }),
});
              
            
              
import { createPinia } from "pinia";

export default createPinia();
              
            

Now we can configure Vuetify. Vuetify is a component library that works really nicely with Vue. The components are highly customizable and it let's us focus more on developing our project than spending time redefining basic components. I can't recommend it enough for solo projects.

In the src directory, create a new folder called plugins. Here we will create two files:

              
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";

export default createVuetify({
  components,
  directives,
});
              
            
              
import vuetify from "./vuetify";
import pinia from "../stores";
import router from "../router";
import type { App } from "vue";

export function registerPlugins(app: App) {
  app.use(vuetify).use(router).use(pinia);
}
            
            

And let's clean up main.ts in the src directory a little bit.

              
import { createApp } from "vue";
import App from "./App.vue";
import { registerPlugins } from "@/plugins";

const app = createApp(App);

registerPlugins(app);

app.mount("#app");
              
            

The final thing to do to use vuetify components is to wrap your app with v-app / v-main. In your App.vue file, modify the template:

              
<template>
  <v-app>
    <v-main>
      <RouterView />
    </v-main>
  </v-app>
</template>
              
            

Now we are good to use vuetify components! Vuetify has some great documentation that covers all of the components available as well as many utility classes and more! That can be found here: https://vuetifyjs.com/

Tying it all together

Now that the initial setup is complete, we can go ahead and start implementing socket.io into our application.

In our server (backend/src/index.ts) update to the following code:

              
import http from "http";
import { Server } from "socket.io";

const server = http.createServer();
const io = new Server(server, {
  cors: {
    origin: "http://localhost:1234",
    methods: ["GET", "POST"],
  },
});

io.on("connection", (socket) => {
  console.log("a user connected");
  socket.on("disconnect", () => {
    console.log("user disconnected");
  });
});

server.listen(3000, () => {
  console.log("listening on *:3000");
});

              
            

It is important to note here that you will have to update the origin ("http://localhost:1234") to match the url/port that you are serving your frontend application off of. Since we are serving the frontend and backend separately, this allows us to securely tell our server that we want to be able to communicate with the client application. For more information about CORS, click here.

Now we just need to build out our frontend and have it access our socket. First, let's update our application router and add in our new view. Replace all of the code in your router's index.ts with the following:

            
import { createRouter, createWebHistory } from "vue-router";
import Chat from "../views/Chat.vue";
const router = createRouter({
  history: createWebHistory(globalThis._importMeta_.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "chat",
      component: Chat,
    },
  ],
});

export default router;
            
          

This view does not exist yet, so we will have to create it. In views, lets add Chat.vue. (You can remove all of the other files in views. In fact, you can actually delete everything in the components directory as well!)

    
<style scoped>
.chat-window {
  height: calc(100vh - 48px);
}

#form {
  background: rgba(0, 0, 0, 0.15);
  padding: 0.25rem;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  height: 3rem;
  box-sizing: border-box;
  backdrop-filter: blur(10px);
}
#input:focus {
  outline: none;
}
#form > button {
  background: #333;
  border: none;
  padding: 0 1rem;
  margin: 0.25rem;
  border-radius: 3px;
  outline: none;
  color: #fff;
}
</style>

<template>
  <v-list id="chat-window" :lines="false" class="pa-0 ma-0 chat-window">
    <v-list-item
      v-for="message in messages"
      :key="uuidv4()"
      :style="`background: ${message.hex}`"
      prepend-avatar="https://placekitten.com/64/64"
    >
      <template v-slot:title>
        <div
          :class="
            isColorDark(message.hex)
              ? 'text-white font-weight-bold'
              : 'text-medium-emphasis font-weight-bold'
          "
        >
        &lcub&lcub message.userId &rcub&rcub
        </div>
        <div :class="isColorDark(message.hex) ? 'text-white' : ''">
          &lcub&lcub message.msg &rcub&rcub
        </div>
      </template>
    </v-list-item>
  </v-list>
  <v-form
    id="form"
    @submit.prevent=""
    class="d-flex align-center justify-space-between"
  >
    <input
      v-model="inputMessage"
      type="text"
      class="bg-white rounded w-100 h-100 mr-4"
      @keydown.enter="onClickSend"
    />
    <v-btn @click="onClickSend">Send</v-btn>
  </v-form>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { io } from "socket.io-client";
import { onBeforeUnmount } from "vue";
import { useAppState } from "@/stores/app";
import { type AllMessage } from "@/types";
import { v4 as uuidv4 } from "uuid";

const $state = useAppState();
const socket = io("http://localhost:3000");
const messages = ref<AllMessage[]>([]);
const inputMessage = ref("");

const scrollToBottom = (elementId: string) => {
  var element = document.getElementById(elementId);
  if (element) {
    element.scrollTop = element.scrollHeight;
  }
};

const isColorDark = (hexColor: string) => {
  if (hexColor.startsWith("#")) {
    hexColor = hexColor.slice(1);
  }
  const red = parseInt(hexColor.slice(0, 2), 16);
  const green = parseInt(hexColor.slice(2, 4), 16);
  const blue = parseInt(hexColor.slice(4, 6), 16);
  const relativeLuminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
  const threshold = 128;

  return relativeLuminance < threshold;
};

const onClickSend = () => {
  if (inputMessage.value) {
    const payload: AllMessage = {
      userId: $state.userId,
      hex: $state.hex,
      msg: inputMessage.value,
    };
    socket.emit("message all", payload);
    inputMessage.value = "";
  }
};

socket.on("message all", async (payload: AllMessage) => {
  await messages.value.push(payload);
  scrollToBottom("chat-window");
});

onBeforeUnmount(() => {
  socket.disconnect();
});
</script>

    
  

Then we will want to update the App.vue component to reference our state variables - as well as set them every time the page is visited. Now, if we had this as an application used by real users, we would want to consider using local storage - however - I am going to omit this so that when we can easily replicate what it would be like if multiple users were using our application just by opening up new instances.

    
<script setup lang="ts">
import { RouterView } from "vue-router";
import { v4 as uuidv4 } from "uuid";
import { useAppState } from "@/stores/app";

const $state = useAppState();

const generateRandomHexColor = () => {
  var randomColor = Math.floor(Math.random() * 16777215).toString(16);
  while (randomColor.length < 6) {
    randomColor = "0" + randomColor;
  }
  randomColor = "#" + randomColor;

  return randomColor;
};

//If you want to have separate tabs open as "different users"
//Then you can't reference localStorage
// let id = localStorage.getItem("userId");
let id = undefined;

// Same here
// let hex = localStorage.getItem("hex");
let hex = undefined;

if (!id) {
  id = uuidv4();
  localStorage.setItem("userId", id);
}
if (!hex) {
  hex = generateRandomHexColor();
  localStorage.setItem("hex", hex);
}

$state.userId = id;
$state.hex = hex;
</script>
    
  


All that is left to do is to update the backend and then you can communicate client to client. First in the backend, lets just add a model for messages in backend/src/types.

              
export interface AllMessage {
  userId: string;
  hex: string;
  msg: string;
}
              
            

And update the code in index.ts to match the following:

            
import http from "http";
import { Server } from "socket.io";
import { AllMessage } from "./types";

const server = http.createServer();
const io = new Server(server, {
  cors: {
    origin: "http://localhost:1234",
    methods: ["GET", "POST"],
  },
});

io.on("connection", (socket) => {
  console.log("a user connected");
  socket.on("disconnect", () => {
    console.log("user disconnected");
  });

  socket.on("message all", (payload: AllMessage) => {
    console.log("payload: ", payload);
    io.emit("message all", payload);
  });
});

server.listen(3000, () => {
  console.log("listening on *:3000");
});

            

And that's it! If you open up the backend and frontend in your terminal and run the following command: npm run dev - then you should be able to open it up in two separate tabs and communicate between clients!

Final thoughts

There were a few key parts that we didn't implement in order to keep this blog post somewhat concise and to the point. For example, in a real application we would most likely want to set up a database and user authentication instead of spinning up a random uuid every time a user accessed our application. I plan to do more posts in the future that will address these topics.

Subscribe.

Like what you are reading? Sign up to recieve my updates straight to your inbox. No spam, unsubscribe anytime!

Share This

Follow me

Michael Rohner © 2025.