WebSockets
Fresh provides built-in helpers for upgrading HTTP connections to WebSockets. There are two main approaches depending on your use case.
Quick start with app.ws()
The simplest way to add a WebSocket endpoint:
import { App } from "fresh";
const app = new App()
.ws("/ws", {
open(socket) {
console.log("Client connected");
},
message(socket, event) {
socket.send(`Echo: ${event.data}`);
},
close(socket, code, reason) {
console.log("Client disconnected", code, reason);
},
});app.ws(path, handlers) registers a GET route that automatically upgrades the
request to a WebSocket connection and wires up your event handlers.
Using ctx.upgrade() in route handlers
For file-based routes or when you need more control, use ctx.upgrade() inside
a GET handler.
Managed mode
Pass an event handlers object and receive the upgrade Response directly:
import { define } from "@/utils.ts";
export const handlers = define.handlers({
GET(ctx) {
return ctx.upgrade({
open(socket) {
console.log("Client connected");
},
message(socket, event) {
socket.send(`Echo: ${event.data}`);
},
close(socket, code, reason) {
console.log("Disconnected", code, reason);
},
error(socket, event) {
console.error("WebSocket error", event);
},
});
},
});Bare mode
Call ctx.upgrade() without arguments to get the raw WebSocket object. This
is useful when you need to store the socket in a shared structure like a chat
room or pub/sub registry:
import { define } from "@/utils.ts";
const clients = new Set<WebSocket>();
export const handlers = define.handlers({
GET(ctx) {
const { socket, response } = ctx.upgrade();
socket.onopen = () => {
clients.add(socket);
};
socket.onmessage = (event) => {
// Broadcast to all connected clients
for (const client of clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(event.data);
}
}
};
socket.onclose = () => {
clients.delete(socket);
};
return response;
},
});Upgrade options
Both modes accept an options object to configure the underlying WebSocket:
// Managed mode — pass handlers first, then options
ctx.upgrade(handlers, {
idleTimeout: 60, // close if no ping received within 60s (default: 120)
protocol: "graphql-ws", // sub-protocol to negotiate
});
// Bare mode — pass options without handlers to get the raw socket back
const { socket, response } = ctx.upgrade({ idleTimeout: 60 });How does Fresh tell the two calls apart? The first argument is treated as managed-mode handlers when it contains at least one function-valued handler key (
open,message,close, orerror). A plain options object only has non-function fields (idleTimeout,protocol), so it always enters bare mode.
The same options can be passed to app.ws():
app.ws("/ws", handlers, { idleTimeout: 60 });
app.ws()always uses managed mode. For bare-mode access to the raw socket, useapp.get()withctx.upgrade()instead.
Error handling
If a non-WebSocket request hits a WebSocket route, ctx.upgrade() throws an
HttpError(400) with the message “Expected a WebSocket upgrade request”. This
is handled automatically by Fresh’s error pipeline and returns a 400 response.
Handler reference
All handler callbacks are optional:
| Callback | Arguments | Description |
|---|---|---|
open |
(socket) |
Connection established |
message |
(socket, event) |
Message received (event.data contains the payload) |
close |
(socket, code, reason) |
Connection closed |
error |
(socket, event) |
Error occurred on the connection |
Client-side example
Connect from the browser:
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
ws.onopen = () => {
ws.send("Hello from the client!");
};
ws.onmessage = (event) => {
console.log("Received:", event.data);
};