Building and Deploying a Real-Time WebSocket Chat Application with Deno, Oak, and Render

by rudr
Building and Deploying a Real-Time WebSocket Chat Application with Deno, Oak, and Render
WebsocketRealtimeDeploymentJavascriptDeno

Have you ever wanted to build a real-time chat application but felt overwhelmed by the technical details? Or maybe you’ve wondered how to deploy your app without emptying your wallet?

Well, you're in the right place! Imagine creating a WebSocket chat app from scratch, using the power of Deno and Oak, and then deploying it for free with Render. Sounds pretty awesome, right?

In this article, I'm going to show you how to do just that. We'll walk through setting up the Socket Deno template, running it locally, and deploying it so you can see your app live on the web — all without spending a cent.


Why Deno and Oak?

Honestly speaking, I wanted to try something new, and you should too! I stumbled upon Bun, Hono, and Cloudflare Workers in my journey, but they were a bit tricky for me to get a handle on.

That’s when I discovered the Deno + Oak combo. Deno is a modern, secure runtime for JavaScript and TypeScript that’s super straightforward. Oak is a lightweight framework that makes building web apps a breeze. Pairing them up and deploying on Render? It’s been a game-changer for me.

Let’s dive into why this stack is so awesome and how it can help you build your own real-time chat app with ease.


Check Out the Repo and Set Up Locally

Want to get your hands on the code? Head over to my GitHub repository for the Socket Deno template. The repo has everything you need to get started, including detailed setup instructions.


Understanding the Code

WebSocket Setup
  • connectedClients: Keeps track of all active WebSocket connections with usernames as keys.
  • broadcast(message): Sends a message to all connected clients.
  • broadcast_usernames(): Updates all clients with the current list of usernames.
Handling WebSocket Connections
  • /start_web_socket Route: Upgrades the connection to WebSocket, manages client connections, and handles events like sending and receiving messages.
Connection Events
  • onopen: Updates all clients with new usernames when someone connects.
  • onclose: Removes clients and updates usernames when someone disconnects.
  • onmessage: Processes incoming messages and broadcasts them.
Serving Static Files
  • Serves index.html from the public directory for the root URL.
Starting the Server
  • Listens on port 8080 and logs the URL.

Deploying on Render

Time to go live! Here’s how to deploy your WebSocket chat app on Render:

Push Your Code to GitHub
  • Make sure your code is pushed to a GitHub repo. If you haven’t done that yet, go ahead and upload it.
Set Up on Render
  • Log in to Render and connect it to your GitHub account.
  • Follow the deployment instructions in the README file of my GitHub repo.

And that’s it! Render will handle the rest and provide you with a URL to your live app. Easy peasy!


Full Code

Server.js
import { Application, Router } from "@oak/oak";

const connectedClients = new Map();

const app = new Application();
const port = 8080;
const router = new Router();

function broadcast(message) {
  for (const client of connectedClients.values()) {
    try {
      client.send(message);
    } catch (error) {
      console.error("Error broadcasting message:", error);
    }
  }
}

function broadcast_usernames() {
  const usernames = [...connectedClients.keys()];
  console.log(
    "Sending updated username list to all clients: " + JSON.stringify(usernames)
  );
  broadcast(
    JSON.stringify({
      event: "update-users",
      usernames: usernames,
    })
  );
}

router.get("/start_web_socket", async (ctx) => {
  const socket = await ctx.upgrade();
  const username = ctx.request.url.searchParams.get("username");
  if (connectedClients.has(username)) {
    socket.close(1008, `Username ${username} is already taken`);
    return;
  }
  socket.username = username;
  connectedClients.set(username, socket);
  console.log(`New client connected: ${username}`);

  socket.onopen = () => {
    broadcast_usernames();
  };

  socket.onclose = () => {
    console.log(`Client ${socket.username} disconnected`);
    connectedClients.delete(socket.username);
    broadcast_usernames();
  };

  socket.onmessage = async (m) => {
    try {
      const data = JSON.parse(m.data);
      switch (data.event) {
        case "send-message":
          await broadcast(
            JSON.stringify({
              event: "send-message",
              username: socket.username,
              message: data.message,
            })
          );
          break;
        default:
          console.warn("Unknown event:", data.event);
      }
    } catch (error) {
      console.error("Error handling message:", error);
    }
  };

  socket.onerror = (error) => {
    console.error("WebSocket error:", error);
  };
});

app.use(router.routes());
app.use(router.allowedMethods());
app.use(async (context) => {
  await context.send({
    root: `${Deno.cwd()}/`,
    index: "public/index.html",
  });
});

console.log("Listening at http://localhost:" + port);
await app.listen({ port });

Contributing and License

Want to contribute? Great! If you spot any bugs or have ideas for improvements, feel free to:

  • Submit an Issue: Report any problems or feature requests on the GitHub Issues page.
  • Send a Pull Request: Got a fix or new feature? Submit a pull request with your changes, and I’ll review and merge them.
  • License: This project is licensed under the MIT License.

Wrap-Up and Thanks!

That's a wrap — your WebSocket chat app is live! If you enjoyed this guide, hit the star on my GitHub repo to show some love & support.

Feel free to tweak, experiment, and have fun with the code. If you need help or just want to chat, drop a comment or reach out.

Happy coding and see you around!