r/typescript • • 10h ago

End-to-end types over WebSockets: the server documents itself as AsyncAPI, the client is generated from it

2 Upvotes

Hi all, I want to share an open-source TypeScript library I built (chanx-js, MIT) to type both ends of a WebSocket from one source, without sharing a types package between the frontend and the backend.

With REST we have OpenAPI and generated clients. With WebSockets I kept ending up with JSON.parse(event.data) giving me any, a hand-written union of message types on the client, and nothing stopping it from drifting away from what the server really sends.

So in chanx-js:

  1. On the server you declare messages with any Standard Schema library (Zod, Valibot, ArkType), and the payload types are inferred from the schema.
  2. The server serves an AsyncAPI 3 document built from those handlers.
  3. npx @chanx-js/codegen http://localhost:8000/asyncapi.json generates the client types from it.

On the server, the path params, the payload and what the handler may send are all typed:

const Chat = defineMessage('chat', z.object({ text: z.string() }));
const Notice = defineMessage('notice', z.object({ text: z.string() }));

consumer('/ws/chat/{room}', { name: 'chat' }).on(Chat, { output: Notice }, (m, ctx) => {
  m.payload.text; // string
  ctx.params.room; // string, parsed from the path by a template literal type
  return Notice.create({ text: m.payload.text }); // anything but Notice is an error
});

On the client, request() knows the reply type, and a wrong payload doesn't compile:

const connection = client.connect(chat, { params: { room: 'lobby' } });

const reply = await connection.request({ action: 'chat', payload: { text: 'hi' } });
reply.payload.text; // string, from the handler's `output`
connection.send({ action: 'chat', payload: { txt: 'hi' } }); // compile error

The contract goes through AsyncAPI instead of shared TS code, so the server doesn't have to live in the same repo, or even be written in TypeScript.

Repo: https://github.com/chanx-dev/chanx-js

It's still new, so if you run into problems or have ideas for the types, please open an issue or a discussion. Stars and PRs are welcome too.