Skip to content

Client

shiftapi prepare generates a fully-typed websocket function constrained to WebSocket paths only — calling websocket on a non-WebSocket path is a compile-time error.

websocket returns a WSConnection with typed send, receive, async iteration, and close:

import { websocket, WSError } from "@shiftapi/client";
const ws = websocket("/ws", {
params: { query: { token: "abc" } },
});
// Send a message
await ws.send({ text: "hello" });
// Receive a single message
const msg = await ws.receive();
console.log(msg);
// ^? EchoReply — fully typed
// Or iterate over all messages
try {
for await (const msg of ws) {
console.log(msg.text);
}
} catch (err) {
if (err instanceof WSError) console.error(err.message);
}

To close the connection:

ws.close(1000, "done");

Path parameters are type-checked. If your Go handler declares path:"room" on the input struct, the generated types require it:

const ws = websocket("/rooms/{room}", {
params: {
path: { room: "general" },
query: { token: "secret" },
},
});

Connections automatically reconnect on unexpected disconnections using exponential backoff. This covers network blips, server restarts, and dev-server hot reloads. send() throws if the connection is not currently open — messages are never silently buffered, since server state may have been lost.

Use onopen and onclose to observe connection state:

const ws = websocket("/ws");
ws.onopen = () => console.log("connected");
ws.onclose = () => console.log("disconnected, reconnecting...");

Reconnection does not occur when:

  • You call ws.close() explicitly
  • The server sends a WSError (application-level rejection like 4401 or 4422)

To customize or disable reconnection:

// Disable reconnection
const ws = websocket("/ws", { reconnect: false });
// Custom backoff
const ws = websocket("/ws", {
reconnect: {
maxRetries: 5, // give up after 5 attempts (default: Infinity)
baseDelay: 500, // first retry after 500ms (default: 1000)
maxDelay: 10000, // cap backoff at 10s (default: 30000)
},
});
OptionDefaultDescription
reconnecttruetrue to reconnect with defaults, false to disable, or a ReconnectOptions object
reconnect.maxRetriesInfinityMaximum reconnect attempts before giving up
reconnect.baseDelay1000Initial delay in ms (doubled each attempt)
reconnect.maxDelay30000Maximum delay in ms between attempts
PropertyDefaultDescription
onopennullCalled when the connection opens (including after reconnection)
onclosenullCalled when the connection closes unexpectedly (not after explicit close())

When the server rejects a connection (input validation, setup errors, or registered error types), it sends a structured error frame before closing with a 4xxx code. The client wraps this in a WSError:

import { websocket, WSError } from "@shiftapi/client";
const ws = websocket("/ws");
try {
for await (const msg of ws) {
console.log(msg);
}
} catch (err) {
if (err instanceof WSError) {
if (err.code === 4401) {
console.log(err.details.realm);
// ^? string — narrowed to AuthError
}
}
}
PropertyTypeDescription
codeCodeThe WebSocket close code (e.g. 4401, 4422). Narrows to a literal type when error types are registered via WithError.
messagestringThe error message from the server
detailsDetailsThe structured error payload. Narrows to the registered error type based on code when using the generated WSErrorFor<P> type.

When the server closes the connection with a 4xxx code, the error payload is wrapped in a WSError. Input errors are thrown from for await, receive(), and send(). Normal disconnects stop iteration silently.

import { websocket, WSError } from "@shiftapi/client";
const ws = websocket("/rooms/{room}", {
params: { path: { room: "general" } },
// missing required query param "token"
});
try {
for await (const msg of ws) {
console.log(msg);
}
} catch (err) {
if (err instanceof WSError) {
console.log(err.code); // 4422
console.log(err.message); // "validation failed"
console.log(err.details);
// { message: "validation failed", errors: [{ field: "Token", message: "this field is required" }] }
}
}

The generated WSChannels interface includes an errors map for each path, and the codegen produces a WSErrorFor<P> type that narrows WSError by close code:

interface WSChannels {
"/ws": {
send: components["schemas"]["EchoRequest"];
receive: components["schemas"]["EchoReply"];
errors: {
4401: components["schemas"]["AuthError"];
4422: components["schemas"]["ValidationError"];
};
};
}

For endpoints that send multiple message types (registered with WSSends on the Go side), the generated client narrows message types based on the type field:

import { websocket } from "@shiftapi/client";
const ws = websocket("/chat");
for await (const msg of ws) {
if (msg.type === "chat") {
console.log(msg.data.text);
// ^? string — narrowed to ChatMessage
} else if (msg.type === "system") {
console.log(msg.data.info);
// ^? string — narrowed to SystemMessage
}
}

Use websocket with React state for a chat-style component. The onClose callback drives a “reconnecting” indicator, and onOpen clears it:

import { websocket, WSError } from "@shiftapi/client";
import { useEffect, useRef, useState } from "react";
function Chat() {
const [messages, setMessages] = useState<EchoReply[]>([]);
const [error, setError] = useState<WSError | null>(null);
const [connected, setConnected] = useState(false);
const wsRef = useRef<ReturnType<typeof websocket<"/ws">> | null>(null);
useEffect(() => {
const ws = websocket("/ws");
ws.onopen = () => setConnected(true);
ws.onclose = () => setConnected(false);
wsRef.current = ws;
(async () => {
try {
for await (const msg of ws) {
setMessages((prev) => [...prev, msg]);
}
} catch (err) {
if (err instanceof WSError) setError(err);
}
})();
return () => ws.close();
}, []);
const send = (text: string) => {
wsRef.current?.send({ text });
};
if (error) return <div>Connection error: {error.message}</div>;
return (
<div>
{!connected && <div>Reconnecting…</div>}
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg.text}</li>
))}
</ul>
<button onClick={() => send("hello")} disabled={!connected}>
Send
</button>
</div>
);
}