Skip to content

Route parameters are extracted from the URL path as strings - that's just how URLs work. But you often need more than a string: a numeric ID, a positive integer, a value from a fixed set or of a specific pattern, uuid, date, etc.

KosmoJS lets you express these requirements directly in the type system.

Params Refinements

Pass a tuple as the second type argument to defineRoute. Each position refines the corresponding parameter in route order.

For a route at api/users/[id]/index.ts:

api/users/[id]/index.ts
ts
import { defineRoute } from "_/api";

export default defineRoute<"users/[id]", [
  number 
]>(({ GET }) => [
  GET(async (ctx) => {
    const { id } = ctx.validated.params; // typed as number
  }),
]);

A request to /api/users/abc is rejected with a 400 before your handler runs.

Numeric params are coerced for you

A URL param arrives as a string, but when you type it as number, KosmoJS coerces it before validation - "123" becomes 123.

A non-numeric value like "abc" is left as-is and fails the number check, producing a clean 400.

So you write number (or a numeric VRefine) and read a real number from ctx.validated.params; no manual coercion needed.

Access validated parameters through ctx.validated.params - it carries the refined type, not the raw string. The underlying raw params still exists if you need the original:

  • Hono - ctx.req.param()
  • H3 - event.context.params
  • Koa - ctx.params

Refine further with VRefine (globally available, no import needed):

api/users/[id]/index.ts
ts
export default defineRoute<"users/[id]", [
  VRefine<number, { minimum: 1, multipleOf: 1 }> // positive integer
]>(({ GET }) => [
  // ...
]);

More on VRefine ›

Multiple Parameters

For routes with multiple parameters, each tuple position maps to the corresponding param. Positions are optional - omit any you don't need to refine.

api/users/[id]/[view]/index.ts
ts
export default defineRoute<"users/[id]/[view]", [
  VRefine<number, { minimum: 1, multipleOf: 1 }>, // id
  "profile" | "settings" | "data"                 // view
]>(({ GET }) => [
  GET(async (ctx) => {
    const { id, view } = ctx.validated.params;
  }),
]);

Refinements are positional, not name-based - renaming [id] to [userId] requires no changes here.

Released under the MIT License.