Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ export {
// ------ Utils ------

// Route
export { defineRoute } from "./utils/route.ts";
export type { RouteDefinition } from "./utils/route.ts";
export { defineRoute, defineWebSocketRoute } from "./utils/route.ts";
export type {
RouteDefinition,
WebSocketRouteDefinition,
} from "./utils/route.ts";

// Request
export {
Expand Down
64 changes: 64 additions & 0 deletions src/utils/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { HTTPMethod } from "../types/h3.ts";
import type { EventHandler, Middleware } from "../types/handler.ts";
import type { H3Plugin, H3 } from "../types/h3.ts";
import type { StandardSchemaV1 } from "./internal/standard-schema.ts";
import type { Hooks as WSHooks } from "crossws";
import { defineValidatedHandler } from "../handler.ts";
import { defineWebSocketHandler } from "./ws.ts";

/**
* Route definition options
Expand Down Expand Up @@ -69,3 +71,65 @@ export function defineRoute(def: RouteDefinition): H3Plugin {
h3.on(def.method, def.route, handler);
};
}

/**
* WebSocket route definition options
*/
export interface WebSocketRouteDefinition {
/**
* HTTP method for the route (typically 'GET' for WebSocket upgrades)
*/
method?: HTTPMethod;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebSocket handlers should not accept a method.


/**
* Route pattern, e.g. '/api/ws'
*/
route: string;

/**
* WebSocket hooks
*/
websocket: Partial<WSHooks>;

/**
* Optional middleware to run before WebSocket upgrade
*/
middleware?: Middleware[];

/**
* Additional route metadata
*/
meta?: Record<string, unknown>;
Copy link
Member

@pi0 pi0 Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Middleware and meta are not supported. Types should be removed (at least untill they are supported by defineWebSocket

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should take the comment line and leave a TODO ?

}

/**
* Define a WebSocket route as a plugin that can be registered with app.register()
*
* @example
* ```js
* const wsRoute = defineWebSocketRoute({
* route: '/api/ws',
* websocket: {
* open: (peer) => {
* console.log('WebSocket connected:', peer.id);
* peer.send('Welcome!');
* },
* message: (peer, message) => {
* console.log('Received:', message);
* peer.send(`Echo: ${message}`);
* },
* close: (peer) => {
* console.log('WebSocket closed:', peer.id);
* }
* }
* });
*
* app.register(wsRoute);
* ```
*/
export function defineWebSocketRoute(def: WebSocketRouteDefinition): H3Plugin {
const handler = defineWebSocketHandler(def.websocket);
return (h3: H3) => {
h3.on(def.method || "GET", def.route, handler);
};
}
115 changes: 114 additions & 1 deletion test/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { defineRoute } from "../src/utils/route.ts";
import { defineRoute, defineWebSocketRoute } from "../src/utils/route.ts";
import { H3 } from "../src/h3.ts";
import { z } from "zod";

Expand Down Expand Up @@ -76,3 +76,116 @@ describe("defineRoute", () => {
});
});
});

describe("defineWebSocketRoute", () => {
it("should create a plugin that registers WebSocket route", async () => {
const app = new H3();
const wsRoute = defineWebSocketRoute({
route: "/ws",
websocket: {
open: () => {},
message: () => {},
},
});
app.register(wsRoute);

const route = app._routes.find((r) => r.route === "/ws");
expect(route).toBeDefined();
expect(route?.method).toBe("GET");
});

it("should use GET method by default for WebSocket routes", async () => {
const app = new H3();
const wsRoute = defineWebSocketRoute({
route: "/ws",
websocket: {},
});
app.register(wsRoute);

const route = app._routes.find((r) => r.route === "/ws");
expect(route?.method).toBe("GET");
});

it("should allow custom HTTP method for WebSocket routes", async () => {
const app = new H3();
const wsRoute = defineWebSocketRoute({
method: "POST",
route: "/ws",
websocket: {},
});
app.register(wsRoute);

const route = app._routes.find((r) => r.route === "/ws");
expect(route?.method).toBe("POST");
});

it("should test WebSocket upgrade response", async () => {
const app = new H3();
const wsRoute = defineWebSocketRoute({
route: "/ws",
websocket: {
open: (peer) => {
peer.send("Welcome!");
},
},
});
app.register(wsRoute);

const res = await app.fetch("/ws");
expect(res.status).toBe(426);
expect(await res.text()).toContain("WebSocket upgrade is required");
expect((res as any).crossws).toBeDefined();
});

it("should work with different route patterns", async () => {
const app = new H3();
const patterns = ["/api/ws", "/ws/:id", "/chat/:room/:user"];

for (const pattern of patterns) {
const wsRoute = defineWebSocketRoute({
route: pattern,
websocket: {},
});
app.register(wsRoute);

const route = app._routes.find((r) => r.route === pattern);
expect(route).toBeDefined();
expect(route?.route).toBe(pattern);
}
});

it("should be compatible with existing WebSocket handler methods", async () => {
const app = new H3();
const hooks = {
open: () => {},
message: () => {},
close: () => {},
};

// Test compatibility with defineWebSocketRoute
const wsRoute = defineWebSocketRoute({
route: "/new-ws",
websocket: hooks,
});
app.register(wsRoute);

// Test compatibility with traditional approach
const { defineWebSocketHandler } = await import("../src/index.ts");
app.on("GET", "/old-ws", defineWebSocketHandler(hooks));

// Both routes should be registered
const newRoute = app._routes.find((r) => r.route === "/new-ws");
const oldRoute = app._routes.find((r) => r.route === "/old-ws");

expect(newRoute).toBeDefined();
expect(oldRoute).toBeDefined();

// Both should return similar responses
const newRes = await app.fetch("/new-ws");
const oldRes = await app.fetch("/old-ws");

expect(newRes.status).toBe(oldRes.status);
expect((newRes as any).crossws).toBeDefined();
expect((oldRes as any).crossws).toBeDefined();
});
});
1 change: 1 addition & 0 deletions test/unit/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe("h3 package", () => {
"defineValidatedHandler",
"defineWebSocket",
"defineWebSocketHandler",
"defineWebSocketRoute",
"deleteCookie",
"dynamicEventHandler",
"eventHandler",
Expand Down