Skip to content

Tutorial 2 · Connect a typed client

Tutorials1 · Run a server2 · Connect a typed client3 · Make it React

The server from Tutorial 1 is waiting. Now the client imports the same contract — so the request, the event, and the topic are all inferred, end to end, with zero codegen. You'll exercise all three patterns over one connection, and watch the server refuse a payload TypeScript would never have let you write.

~6 minutesBuilds on Tutorial 1TypeScript · zero codegen

Request send()Event on('message')Topic subscribe('presence')

First, see it run

Tutorial 1's server, plus two real clients — ada and bob. Join the room on both sides and talk; toggle the presence topic off and watch deliveries stop for that client only; then press send an invalid payload to see the server's answer to a hand-crafted bad frame.

tutorial 2 · two clients, three wire patternsbooting…
ada

no presence yet

not in a room yet — join to receive broadcasts

bob

no presence yet

not in a room yet — join to receive broadcasts

What's real here: the same server as Tutorial 1 plus two real createSuperLineClient connections — real requests, a real pushed event, a real topic subscription, and real server-side validation on the invalid send. In-tab substitution: the loopback wire instead of WebSocket.

Notice bob receives nothing until he joins — room membership lives on the server, not in the client. And the invalid payload comes back as a typed SuperLineError: types make the wrong thing hard to write, but it's the server's re-validation that makes it impossible to slip through.

1. Add the client package

In the my-line project from Tutorial 1:

bash
pnpm add @super-line/client
bash
npm install @super-line/client
bash
yarn add @super-line/client

And a script for it in package.json:

json
"scripts": {
  "server": "tsx src/server.ts",
  "client": "tsx src/client.ts"
}

2. Write the client

The client imports the same contract.ts, so join, send, on, and subscribe are all inferred — a wrong event name or a bad payload is a compile error, not a runtime surprise.

ts
import { createSuperLineClient } from '@super-line/client'
import { webSocketClientTransport } from '@super-line/transport-websocket'
import { chat } from './contract'

const client = createSuperLineClient(chat, {
  transport: webSocketClientTransport({ url: 'ws://localhost:3000' }),
  role: 'user', // narrows the surface to shared ∪ user; verified by authenticate
  params: { name: 'ada' }, // carried in the handshake → readable as h.query.name
})

client.on('message', (m) => console.log(`💬 ${m.from}: ${m.text}`)) // event
client.subscribe('presence', (p) => console.log(`👥 ${p.count} online in ${p.room}`)) // topic

await client.join({ room: 'lobby' })
await client.send({ room: 'lobby', text: 'hello, super-line' }) // request → typed { id }

await new Promise((r) => setTimeout(r, 300)) // let the pushes land, then exit
client.close()

Node 18 / 20: provide a WebSocket

The client uses the global WebSocket, which exists in browsers and Node 22+. On older Node, install ws and pass it through: webSocketClientTransport({ url, WebSocket }).

3. Run the round-trip

Start the server, then the client in a second terminal:

bash
npm run server
bash
npm run client

The client prints:

👥 1 online in lobby
💬 ada: hello, super-line

That's a full typed round-trip.

One contract, three wire patterns, end to end. The presence line is a topic the server pushed on join; the ada: … line is an event broadcast from your send request — all over a single connection, with zero codegen.

What just happened

Your client callPatternWhat it does
await client.send(…)RequestValidated input in, typed { id } back — like an RPC.
client.on('message', …)EventThe server pushes; you listen. Fire-and-forget.
client.subscribe('presence', …)TopicYou opt in; the server fans out to every subscriber. Unsubscribe and deliveries stop — try the toggle in the demo.

Rename a field in contract.ts and the other side stops compiling — that's the contract earning its keep. And types aren't trust: every inbound payload is re-validated against the schema on the server, so even an untyped peer can't slip a bad message through. That's what the demo's invalid payload button proves — it bypasses TypeScript on purpose and gets a SuperLineError back.

Next: put a UI on it

Console logs prove the wire works. Real apps render it — and the React binding turns each of these three patterns into a hook.

Continue the series

Tutorial 3 · Make it React → — typed hooks for requests, events, topics, and live data — with two real React apps running on this site.

Or branch off from here

Released under the MIT License.