JavaScript / TypeScript quick start

Expose echo and a reverse callback at ws://127.0.0.1:8000/ws/. The complete examples use Node.js 24+ to run .ts directly; the compiled library supports Node.js 22+.

Install

Choose a framework. The install and server tabs stay in sync. Before the first npm release, use the local package archives.

npm install @wsrpc/server @wsrpc/client
npm install @wsrpc/server @wsrpc/client express
npm install @wsrpc/server @wsrpc/client fastify

Start the server

Save the selected complete example as app.ts. Each handles ordinary HTTP GET /health, registers its RPC methods and closes connections on shutdown.

import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
import { WebSocketAsync } from "@wsrpc/server";

class RPC extends WebSocketAsync {}
RPC.addRoute("echo", (_socket, params: Record<string, unknown>) => params);
RPC.addRoute("callback", (socket, { value }: { value: number }) =>
  socket.call<number>("double", { value }),
);

export function createApp() {
  const server = createServer((request, response) => {
    response.writeHead(request.url === "/health" ? 200 : 404, {
      "content-type": "application/json",
    });
    response.end(
      JSON.stringify(
        request.url === "/health" ? { ok: true } : { error: "Not found" },
      ),
    );
  });
  const rpc = RPC.attach(server, { path: "/ws/" });
  return { server, rpc };
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  const { server, rpc } = createApp();
  server.listen(Number(process.env.PORT ?? 8000), "127.0.0.1");
  process.once("SIGINT", () => {
    void rpc.close().then(() => server.close());
  });
}
node app.ts
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
import express from "express";
import { WebSocketAsync } from "@wsrpc/server";

class RPC extends WebSocketAsync {}
RPC.addRoute("echo", (_socket, params: Record<string, unknown>) => params);
RPC.addRoute("callback", (socket, { value }: { value: number }) =>
  socket.call<number>("double", { value }),
);

export function createApp() {
  const app = express();
  app.get("/health", (_request, response) => {
    response.json({ ok: true });
  });
  const server = createServer(app);
  const rpc = RPC.attach(server, { path: "/ws/" });
  return { app, server, rpc };
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  const { server, rpc } = createApp();
  server.listen(Number(process.env.PORT ?? 8000), "127.0.0.1");
  process.once("SIGINT", () => {
    void rpc.close().then(() => server.close());
  });
}
node app.ts
import { pathToFileURL } from "node:url";
import Fastify from "fastify";
import { WebSocketAsync } from "@wsrpc/server";

class RPC extends WebSocketAsync {}
RPC.addRoute("echo", (_socket, params: Record<string, unknown>) => params);
RPC.addRoute("callback", (socket, { value }: { value: number }) =>
  socket.call<number>("double", { value }),
);

export function createApp() {
  const app = Fastify();
  app.get("/health", async () => ({ ok: true }));
  const rpc = RPC.attach(app.server, { path: "/ws/" });
  app.addHook("preClose", async () => {
    await rpc.close();
  });
  return { app, rpc };
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  const { app } = createApp();
  await app.listen({
    port: Number(process.env.PORT ?? 8000),
    host: "127.0.0.1",
  });
  process.once("SIGINT", () => {
    void app.close();
  });
}
node app.ts

Connect a Node client

Save as client.ts and run node client.ts in another terminal:

import { WSRPCClient } from "@wsrpc/client/node";

const client = await WSRPCClient.connect(
  process.argv[2] ?? "ws://127.0.0.1:8000/ws/",
  {
    routes: new Map([
      ["double", (_socket, { value }: { value: number }) => value * 2],
    ]),
  },
);
try {
  console.log(await client.call("echo", { message: "Hello" }));
  console.log(await client.call("callback", { value: 21 }));
} finally {
  await client.close();
}

It prints { message: 'Hello' } and 42. The callback route is registered before connecting, so the server can call the client immediately. The finally block closes the connection.

For a frontend, use the browser client. Continue with typed contracts, route classes and implicit context. The Python clients can call this server too.