QUESTPIE
CodeRoutes

Types and tooling

The inference helpers, the two functions that run a route without an HTTP request, and the one that lists every route in the app.

View markdown

What exists

NameFromFor
route, RouteBuilderquestpie/servicesWriting a route
executeJsonRoute, executeRawRoutequestpie/servicesRunning one from code
evaluateRouteAccessquestpie/servicesChecking a rule on its own
isJsonRoute, isRawRoutequestpie/servicesNarrowing a definition
introspectRoutes, IntrospectedRoutequestpie/introspectionListing every route
InferRouteInput, InferRouteOutputquestpie/typesReading a route's types
InferRouteParams, RouteParamsFromKeyquestpie/typesReading a route's URL params
RouteMeta, RouteMcpMeta, RouteAccessquestpie/servicesTyping what you pass in

Everything on questpie/services is also on the bare questpie entry.

Inferring a route's types

On a JSON route the helpers read the definition's schema members, not the handler. That matters. Codegen erases handler bodies, and these types still resolve afterwards.

import type {
	InferRouteInput,
	InferRouteOutput,
	InferRouteParams,
} from "questpie/types";

import type createBooking from "@/questpie/server/routes/create-booking.post";

type Input = InferRouteInput<typeof createBooking>;
type Output = InferRouteOutput<typeof createBooking>;
type Params = InferRouteParams<typeof createBooking>;
HelperOn a JSON routeOn a raw route
InferRouteInputThe .schema() typenever
InferRouteOutputThe output or handler returnResponse
InferRouteParamsWhatever .params<…>() saidSame

InferRouteOutput falls back to unknown when it can recover nothing, never to any. So you have to narrow it, instead of quietly getting it wrong.

Params from the file name

RouteParamsFromKey derives params from a route key, brackets and method suffix included. Codegen uses it so client.routes.* types a [id] route correctly, without your handler having declared anything.

type P = RouteParamsFromKey<"users/[id]/posts:GET">; // { id: string }
type Q = RouteParamsFromKey<"auth/[...path]:POST">; // { path: string }

This types the generated surface, not your handler body. Inside the route file you still declare .params<{ id: string }>().

Running a route without HTTP

executeJsonRoute and executeRawRoute run the same pipeline the HTTP handler runs: parse, access check, handler inside the context scope, output check. Use them in a test, a script, or one route calling another.

In a test
import { executeJsonRoute } from "questpie/services";

import subscribe from "@/questpie/server/routes/subscribe.post";

const result = await executeJsonRoute(app, subscribe, {
	email: "ada@example.com",
});
// result: { id: string; status: "subscribed" }
ArgumentexecuteJsonRouteexecuteRawRoute
1the appthe app
2the route definitionthe route definition
3the inputa Request
4a RequestContexta RequestContext
5a Requestthe URL params
6the URL paramsnone

Arguments 4 and up are optional. Pass no context and the call runs with accessMode: "system", which is the right default for a script. Pass one from a real request and the route sees that caller's session.

Your access rule still runs

These helpers are not a back door. .access() is evaluated exactly as it is over HTTP, and a denial throws ApiError.forbidden. Only the access mode differs, and only when you supply no context.

Listing every route

introspectRoutes(source) flattens the routes tree into an array sorted by key. @questpie/mcp reads it to build its tool list. It takes the app, a { routes } object, or the bare tree.

import { introspectRoutes } from "questpie/introspection";

const routes = introspectRoutes(app);
FieldExample
keyposts/[id]
patternposts/:id
path/posts/:id
params["id"]
methods["GET"]
mode"json" or "raw"
metayour .meta() object
definitionthe route itself

methods is always an array, even for the single-method route that one file produces.

  • Client SDK, where these same types land for the caller.
  • Codegen, what erases the handler and why the helpers are built to survive it.

On this page