Guide

WebSocket

Nitro natively supports a cross platform WebSocket API

Nitro natively supports runtime agnostic WebSocket API using CrossWS and H3 WebSocket.

Read more in WebSocket in MDN.
Read more in CrossWS.

Opt-in to the experimental feature

WebSockets support is currently experimental. See nitrojs/nitro#2171 for platform support status.

In order to enable websocket support you need to enable the experimental websocket feature flag.

export default defineNitroConfig({
  experimental: {
    websocket: true
  }
})

Usage

Create a websocket handler in routes/_ws.ts (or server/routes/_ws.ts for Nuxt).

You can use any route like routes/chatroom.ts to register upgrade handler on /chatroom.
_ws.ts
export default defineWebSocketHandler({
  open(peer) {
    peer.send({ user: "server", message: `Welcome ${peer}!` });
    peer.publish("chat", { user: "server", message: `${peer} joined!` });
    peer.subscribe("chat");
  },
  message(peer, message) {
    if (message.text().includes("ping")) {
      peer.send({ user: "server", message: "pong" });
    } else {
      const msg = {
        user: peer.toString(),
        message: message.toString(),
      };
      peer.send(msg); // echo
      peer.publish("chat", msg);
    }
  },
  close(peer) {
    peer.publish("chat", { user: "server", message: `${peer} left!` });
  },
});
Nitro allows you defining multiple websocket handlers using same routing of event handlers.

Use a client to connect to server. Example: (routes/websocket.ts or server/routes/websocket.ts for Nuxt)

index.ts
export default defineEventHandler(() => {
  return /* html */ `<!doctype html>
  <html lang="en" data-theme="dark">
    <head>
      <title>CrossWS Test Page</title>
      <script src="https://cdn.tailwindcss.com"></script>
      <style>
        body {
          background-color: #1a1a1a;
        }
      </style>
      <script type="module">
        // https://github.com/vuejs/petite-vue
        import {
          createApp,
          reactive,
          nextTick,
        } from "https://esm.sh/petite-vue@0.4.1";

        let ws;

        const store = reactive({
          message: "",
          messages: [],
        });

        const scroll = () => {
          nextTick(() => {
            const el = document.querySelector("#messages");
            el.scrollTop = el.scrollHeight;
            el.scrollTo({
              top: el.scrollHeight,
              behavior: "smooth",
            });
          });
        };

        const format = async () => {
          for (const message of store.messages) {
            if (!message._fmt && message.text.startsWith("{")) {
              message._fmt = true;
              const { codeToHtml } = await import("https://esm.sh/shiki@1.0.0");
              const str = JSON.stringify(JSON.parse(message.text), null, 2);
              message.formattedText = await codeToHtml(str, {
                lang: "json",
                theme: "dark-plus",
              });
            }
          }
        };

        const log = (user, ...args) => {
          console.log("[ws]", user, ...args);
          store.messages.push({
            text: args.join(" "),
            formattedText: "",
            user: user,
            date: new Date().toLocaleString(),
          });
          scroll();
          format();
        };

        const connect = async () => {
          const isSecure = location.protocol === "https:";
          const url = (isSecure ? "wss://" : "ws://") + location.host + "/_ws";
          if (ws) {
            log("ws", "Closing previous connection before reconnecting...");
            ws.close();
            clear();
          }

          log("ws", "Connecting to", url, "...");
          ws = new WebSocket(url);

          ws.addEventListener("message", async (event) => {
            let data = typeof event.data === "string" ? data : await event.data.text();
            const { user = "system", message = "" } = data.startsWith("{")
              ? JSON.parse(data)
              : { message: data };
            log(
              user,
              typeof message === "string" ? message : JSON.stringify(message),
            );
          });

          await new Promise((resolve) => ws.addEventListener("open", resolve));
          log("ws", "Connected!");
        };

        const clear = () => {
          store.messages.splice(0, store.messages.length);
          log("system", "previous messages cleared");
        };

        const send = () => {
          console.log("sending message...");
          if (store.message) {
            ws.send(store.message);
          }
          store.message = "";
        };

        const ping = () => {
          log("ws", "Sending ping");
          ws.send("ping");
        };

        createApp({
          store,
          send,
          ping,
          clear,
          connect,
          rand: Math.random(),
        }).mount();

        await connect();
      </script>
    </head>
    <body class="h-screen flex flex-col justify-between">
      <main v-scope="{}">
        <!-- Messages -->
        <div id="messages" class="flex-grow flex flex-col justify-end px-4 py-8">
          <div class="flex items-center mb-4" v-for="message in store.messages">
            <div class="flex flex-col">
              <p class="text-gray-500 mb-1 text-xs ml-10">{{ message.user }}</p>
              <div class="flex items-center">
                <img
                  :src="'https://www.gravatar.com/avatar/' + encodeURIComponent(message.user + rand) + '?s=512&d=monsterid'"
                  alt="Avatar"
                  class="w-8 h-8 rounded-full"
                />
                <div class="ml-2 bg-gray-800 rounded-lg p-2">
                  <p
                    v-if="message.formattedText"
                    class="overflow-x-scroll"
                    v-html="message.formattedText"
                  ></p>
                  <p v-else class="text-white">{{ message.text }}</p>
                </div>
              </div>
              <p class="text-gray-500 mt-1 text-xs ml-10">{{ message.date }}</p>
            </div>
          </div>
        </div>

        <!-- Chatbox -->
        <div
          class="bg-gray-800 px-4 py-2 flex items-center justify-between fixed bottom-0 w-full"
        >
          <div class="w-full min-w-6">
            <input
              type="text"
              placeholder="Type your message..."
              class="w-full rounded-l-lg px-4 py-2 bg-gray-700 text-white focus:outline-none focus:ring focus:border-blue-300"
              @keydown.enter="send"
              v-model="store.message"
            />
          </div>
          <div class="flex">
            <button
              class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4"
              @click="send"
            >
              Send
            </button>
            <button
              class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4"
              @click="ping"
            >
              Ping
            </button>
            <button
              class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4"
              @click="connect"
            >
              Reconnect
            </button>
            <button
              class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-r-lg"
              @click="clear"
            >
              Clear
            </button>
          </div>
        </div>
      </main>
    </body>
  </html>`;
});

Now you can try it on /websocket route!

Check out our chat demo using Nitro Websocket API.

Server-Sent Events (SSE)

As an alternative to WebSockets, you can use Server-sent events

Example

Create an SSE handler in routes/sse.ts (or server/routes/sse.ts for Nuxt).

sse.ts
export default defineEventHandler(async (event) => {
  const eventStream = createEventStream(event)

  const interval = setInterval(async () => {
    await eventStream.push(`Message @ ${new Date().toLocaleTimeString()}`)
  }, 1000)

  eventStream.onClosed(async () => {
    clearInterval(interval)
    await eventStream.close()
  })

  return eventStream.send()
})

Then connect to this SSE endpoint from the client

const eventSource = new EventSource('http://localhost:3000/sse')

eventSource.onmessage = (event) => {
  console.log(event.data)
}
Read more in SSE guide in H3.