Skip to content

Commit d80a6b1

Browse files
bartlomiejuclaude
andcommitted
docs: add WebSocket guide and update examples index
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 03e864b commit d80a6b1

3 files changed

Lines changed: 159 additions & 23 deletions

File tree

docs/latest/examples/common-patterns.md

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -175,29 +175,8 @@ export const handler = define.handlers({
175175

176176
## WebSockets
177177

178-
Fresh runs on Deno, so you can upgrade HTTP connections to WebSockets directly:
179-
180-
```ts routes/api/ws.ts
181-
import { define } from "@/utils.ts";
182-
183-
export const handler = define.handlers({
184-
GET(ctx) {
185-
const { socket, response } = Deno.upgradeWebSocket(ctx.req);
186-
187-
socket.onopen = () => {
188-
console.log("Client connected");
189-
};
190-
socket.onmessage = (event) => {
191-
socket.send(`Echo: ${event.data}`);
192-
};
193-
socket.onclose = () => {
194-
console.log("Client disconnected");
195-
};
196-
197-
return response;
198-
},
199-
});
200-
```
178+
Fresh provides first-class WebSocket support via `ctx.upgrade()`. See the full
179+
[WebSocket guide](/docs/examples/websockets) for all options.
201180

202181
## Subdomain routing
203182

docs/latest/examples/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ that you may like in your Fresh project.
1313
- [Sharing state between islands](./examples/sharing-state-between-islands)
1414
- [Active links](./examples/active-links)
1515
- [Session management](./examples/session-management)
16+
- [WebSockets](./examples/websockets)
1617
- [Common Patterns](./examples/common-patterns)

docs/latest/examples/websockets.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---
2+
description: |
3+
Add real-time WebSocket endpoints to your Fresh app with ctx.upgrade() or app.ws().
4+
---
5+
6+
Fresh provides built-in helpers for upgrading HTTP connections to WebSockets.
7+
There are two main approaches depending on your use case.
8+
9+
## Quick start with `app.ws()`
10+
11+
The simplest way to add a WebSocket endpoint:
12+
13+
```ts main.ts
14+
import { App } from "fresh";
15+
16+
const app = new App()
17+
.ws("/ws", {
18+
open(socket) {
19+
console.log("Client connected");
20+
},
21+
message(socket, event) {
22+
socket.send(`Echo: ${event.data}`);
23+
},
24+
close(socket, code, reason) {
25+
console.log("Client disconnected", code, reason);
26+
},
27+
});
28+
```
29+
30+
`app.ws(path, handlers)` registers a GET route that automatically upgrades the
31+
request to a WebSocket connection and wires up your event handlers.
32+
33+
## Using `ctx.upgrade()` in route handlers
34+
35+
For file-based routes or when you need more control, use `ctx.upgrade()` inside
36+
a GET handler.
37+
38+
### Managed mode
39+
40+
Pass an event handlers object and receive the upgrade `Response` directly:
41+
42+
```ts routes/api/ws.ts
43+
import { define } from "@/utils.ts";
44+
45+
export const handlers = define.handlers({
46+
GET(ctx) {
47+
return ctx.upgrade({
48+
open(socket) {
49+
console.log("Client connected");
50+
},
51+
message(socket, event) {
52+
socket.send(`Echo: ${event.data}`);
53+
},
54+
close(socket, code, reason) {
55+
console.log("Disconnected", code, reason);
56+
},
57+
error(socket, event) {
58+
console.error("WebSocket error", event);
59+
},
60+
});
61+
},
62+
});
63+
```
64+
65+
### Bare mode
66+
67+
Call `ctx.upgrade()` without arguments to get the raw `WebSocket` object. This
68+
is useful when you need to store the socket in a shared structure like a chat
69+
room or pub/sub registry:
70+
71+
```ts routes/api/chat.ts
72+
import { define } from "@/utils.ts";
73+
74+
const clients = new Set<WebSocket>();
75+
76+
export const handlers = define.handlers({
77+
GET(ctx) {
78+
const { socket, response } = ctx.upgrade();
79+
80+
socket.onopen = () => {
81+
clients.add(socket);
82+
};
83+
socket.onmessage = (event) => {
84+
// Broadcast to all connected clients
85+
for (const client of clients) {
86+
if (client.readyState === WebSocket.OPEN) {
87+
client.send(event.data);
88+
}
89+
}
90+
};
91+
socket.onclose = () => {
92+
clients.delete(socket);
93+
};
94+
95+
return response;
96+
},
97+
});
98+
```
99+
100+
## Upgrade options
101+
102+
Both modes accept an options object to configure the underlying WebSocket:
103+
104+
```ts
105+
// Managed mode
106+
ctx.upgrade(handlers, {
107+
idleTimeout: 60, // close if no ping received within 60s (default: 120)
108+
protocol: "graphql-ws", // sub-protocol to negotiate
109+
});
110+
111+
// Bare mode
112+
const { socket, response } = ctx.upgrade({
113+
idleTimeout: 60,
114+
protocol: "graphql-ws",
115+
});
116+
```
117+
118+
The same options can be passed to `app.ws()`:
119+
120+
```ts
121+
app.ws("/ws", handlers, { idleTimeout: 60 });
122+
```
123+
124+
## Error handling
125+
126+
If a non-WebSocket request hits a WebSocket route, `ctx.upgrade()` throws an
127+
`HttpError(400)` with the message "Expected a WebSocket upgrade request". This
128+
is handled automatically by Fresh's error pipeline and returns a 400 response.
129+
130+
## Handler reference
131+
132+
All handler callbacks are optional:
133+
134+
| Callback | Arguments | Description |
135+
| --------- | ------------------------ | ---------------------------------------------------- |
136+
| `open` | `(socket)` | Connection established |
137+
| `message` | `(socket, event)` | Message received (`event.data` contains the payload) |
138+
| `close` | `(socket, code, reason)` | Connection closed |
139+
| `error` | `(socket, event)` | Error occurred on the connection |
140+
141+
## Client-side example
142+
143+
Connect from the browser:
144+
145+
```ts
146+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
147+
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
148+
149+
ws.onopen = () => {
150+
ws.send("Hello from the client!");
151+
};
152+
153+
ws.onmessage = (event) => {
154+
console.log("Received:", event.data);
155+
};
156+
```

0 commit comments

Comments
 (0)