Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f55f3cb
refactor(types): make `params` generic in `H3EventContext`
luxass Oct 11, 2025
d2f51df
refactor(types): enhance `H3Event` context to infer router parameters
luxass Oct 11, 2025
adaf236
refactor: make `getRouterParams` and `getRouterParam` work with typed…
luxass Oct 11, 2025
6ccaeb2
refactor(types): simplify `H3Event` context type definition
luxass Oct 11, 2025
8f7ead7
refactor(types): make provided params required in context
luxass Oct 11, 2025
df68165
refactor(types): simplify types for router parameters inference
luxass Oct 11, 2025
385ff92
refactor(types): use overloads for `getRouterParams` and `getRouterPa…
luxass Oct 11, 2025
e455e6c
refactor(types): remove use of any in `getRouterParams` and `getRoute…
luxass Oct 11, 2025
9f4b1ca
refactor(types): reorganize `getRouterParams` and `getRouterParam` fu…
luxass Oct 11, 2025
52667af
refactor(types): enhance `H3Event` context type definition
luxass Oct 12, 2025
f7c9ece
feat(types): enhance route parameter handling in HTTP methods
luxass Oct 12, 2025
4b459bc
test: add tests for route inference
luxass Oct 12, 2025
5dc2b5a
refactor(types): simplify route parameter handling
luxass Oct 12, 2025
0846190
refactor(types): simplify route type definitions
luxass Oct 12, 2025
42a9512
refactor(types): cleanup handler types
luxass Oct 12, 2025
931281a
chore: remove misplaced test
luxass Oct 12, 2025
73b338a
Merge branch 'main' into feat/infer-route-params
luxass Oct 27, 2025
d70375d
Merge branch 'main' into feat/infer-route-params
luxass Oct 27, 2025
204990f
Merge branch 'main' into feat/infer-route-params
luxass Oct 28, 2025
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
6 changes: 5 additions & 1 deletion src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ export class H3Event<
/**
* Event context.
*/
readonly context: H3EventContext;
readonly context: H3EventContext<_RequestT["routerParams"]> & {
params: _RequestT["routerParams"] extends undefined
? undefined
: _RequestT["routerParams"];
};

/**
* @internal
Expand Down
21 changes: 20 additions & 1 deletion src/h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {

import { toRequest } from "./utils/request.ts";
import { toEventHandler } from "./handler.ts";
import type { RouteParams } from "./types/_utils.ts";

export const NoHandler: EventHandler = () => kNotFound;

Expand Down Expand Up @@ -167,18 +168,36 @@ export const H3 = /* @__PURE__ */ (() => {
return this;
}

on<Route extends string>(
method: HTTPMethod | Lowercase<HTTPMethod> | "",
route: Route,
handler: EventHandler<{
routerParams: RouteParams<Route>;
}>,
opts?: RouteOptions,
): this;
on(
method: HTTPMethod | Lowercase<HTTPMethod> | "",
route: string,
handler: HTTPHandler,
opts?: RouteOptions,
): this;
on<Route extends string>(
method: HTTPMethod | Lowercase<HTTPMethod> | "",
route: Route | string,
handler:
| EventHandler<{
routerParams: RouteParams<Route>;
}>
| HTTPHandler,
opts?: RouteOptions,
): this {
const _method = (method || "").toUpperCase();
route = new URL(route, "http://_").pathname;
this["~addRoute"]({
method: _method as HTTPMethod,
route,
handler: toEventHandler(handler)!,
handler: toEventHandler(handler as HTTPHandler)!,
middleware: opts?.middleware,
meta: { ...(handler as EventHandler).meta, ...opts?.meta },
});
Expand Down
10 changes: 10 additions & 0 deletions src/types/_utils.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
import type { InferRouteParams } from "rou3";

export type MaybePromise<T = unknown> = T | Promise<T>;

export type Simplify<T> = { [K in keyof T]: T[K] } & {};
export type RouteParams<T extends string> =
Simplify<InferRouteParams<T>> extends infer _Simplified
? keyof _Simplified extends never
? undefined
: _Simplified
: never;
11 changes: 8 additions & 3 deletions src/types/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import type { Session } from "../utils/session.ts";
import type { H3Route } from "./h3.ts";
import type { ServerRequestContext } from "srvx";

export interface H3EventContext extends ServerRequestContext {
/* Matched router parameters */
params?: Record<string, string>;
export interface H3EventContext<TParams = Record<string, string>>
extends ServerRequestContext {
/**
* Matched route parameters
*
* If there are no parameters, this will be `undefined`.
*/
params?: TParams;

/* Matched middleware parameters */
middlewareParams?: Record<string, string>;
Expand Down
63 changes: 51 additions & 12 deletions src/types/h3.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { H3EventContext } from "./context.ts";
import type { HTTPHandler, EventHandler, Middleware } from "./handler.ts";
import type { HTTPError } from "../error.ts";
import type { MaybePromise } from "./_utils.ts";
import type { MaybePromise, RouteParams } from "./_utils.ts";
import type { FetchHandler, ServerRequest } from "srvx";
// import type { MatchedRoute, RouterContext } from "rou3";
import type { H3Event } from "../event.ts";
Expand All @@ -21,8 +21,32 @@ export type MatchedRoute<T = any> = {

// https://www.rfc-editor.org/rfc/rfc7231#section-4.1
// prettier-ignore
export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";

export type HTTPMethod = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE";

/**
* Interface for HTTP method handlers (GET, POST, PUT, DELETE, etc.).
*
* Automatically infers route parameters from the route pattern and makes them
* available in the event handler context.
*
* @template {H3} This - Passed as `this` to resolve to the declared H3 class type
* rather than the runtime H3 class implementation. This ensures TypeScript uses
* the correct type signature from this declaration file.
*
* NOTE:
* If we used H3 directly in the return type, the bench implementation would
* fail, due to `app._rou3` not being defined on H3.
*/
interface H3HandlerInterface<This extends H3> {
<Route extends string>(
route: Route,
handler: EventHandler<{
routerParams: RouteParams<Route>;
}>,
opts?: RouteOptions,
): This;
(route: string, handler: HTTPHandler, opts?: RouteOptions): This;
}
export interface H3Config {
/**
* When enabled, H3 displays debugging stack traces in HTTP responses (potentially dangerous for production!).
Expand Down Expand Up @@ -155,6 +179,14 @@ export declare class H3 extends H3Core {
/**
* Register a route handler for the specified HTTP method and route.
*/
on<Route extends string>(
method: HTTPMethod | Lowercase<HTTPMethod> | "",
route: Route,
handler: EventHandler<{
routerParams: RouteParams<Route>;
}>,
opts?: RouteOptions,
): this;
on(
method: HTTPMethod | Lowercase<HTTPMethod> | "",
route: string,
Expand All @@ -179,15 +211,22 @@ export declare class H3 extends H3Core {
/**
* Register a route handler for all HTTP methods.
*/
all<Route extends string>(
route: Route,
handler: EventHandler<{
routerParams: RouteParams<Route>;
}>,
opts?: RouteOptions,
): this;
all(route: string, handler: HTTPHandler, opts?: RouteOptions): this;

get(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
post(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
put(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
delete(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
patch(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
head(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
options(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
connect(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
trace(route: string, handler: HTTPHandler, opts?: RouteOptions): this;
get: H3HandlerInterface<this>;
post: H3HandlerInterface<this>;
put: H3HandlerInterface<this>;
delete: H3HandlerInterface<this>;
patch: H3HandlerInterface<this>;
head: H3HandlerInterface<this>;
options: H3HandlerInterface<this>;
connect: H3HandlerInterface<this>;
trace: H3HandlerInterface<this>;
}
34 changes: 30 additions & 4 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ export function getValidatedQuery(
return validateData(query, validate);
}

export function getRouterParams<Event extends H3Event>(
event: Event,
opts?: { decode?: boolean },
): Event extends H3Event<infer R> ? R["routerParams"] : never;
export function getRouterParams<Event extends HTTPEvent>(
event: Event,
opts?: { decode?: boolean },
): NonNullable<H3Event["context"]["params"]>;
/**
* Get matched route params.
*
Expand All @@ -115,21 +123,23 @@ export function getValidatedQuery(
* const params = getRouterParams(event); // { key: "value" }
* });
*/
export function getRouterParams(
event: HTTPEvent,
export function getRouterParams<Event extends HTTPEvent>(
event: Event,
opts: { decode?: boolean } = {},
): NonNullable<H3Event["context"]["params"]> {
// Fallback object needs to be returned in case router is not used (#149)
const context = getEventContext<H3EventContext>(event);
let params = (context.params || {}) as NonNullable<
H3Event["context"]["params"]
>;

if (opts.decode) {
params = { ...params };
for (const key in params) {
params[key] = decodeURIComponent(params[key]);
}
}

return params;
}

Expand Down Expand Up @@ -186,6 +196,21 @@ export function getValidatedRouterParams(
return validateData(routerParams, validate);
}

export function getRouterParam<
Event extends H3Event,
Key extends Event extends H3Event<infer R>
? keyof R["routerParams"] & string
: never,
>(
event: Event,
name: Key,
opts?: { decode?: boolean },
): Event extends H3Event<infer R> ? R["routerParams"][Key] : never;
export function getRouterParam<Event extends HTTPEvent>(
event: Event,
name: string,
opts?: { decode?: boolean },
): string | undefined;
/**
* Get a matched route param by name.
*
Expand All @@ -196,12 +221,13 @@ export function getValidatedRouterParams(
* const param = getRouterParam(event, "key");
* });
*/
export function getRouterParam(
event: HTTPEvent,
export function getRouterParam<Event extends HTTPEvent>(
event: Event,
name: string,
opts: { decode?: boolean } = {},
): string | undefined {
const params = getRouterParams(event, opts);

return params[name];
}

Expand Down
Loading