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.
What exists
| Name | From | For |
|---|---|---|
route, RouteBuilder | questpie/services | Writing a route |
executeJsonRoute, executeRawRoute | questpie/services | Running one from code |
evaluateRouteAccess | questpie/services | Checking a rule on its own |
isJsonRoute, isRawRoute | questpie/services | Narrowing a definition |
introspectRoutes, IntrospectedRoute | questpie/introspection | Listing every route |
InferRouteInput, InferRouteOutput | questpie/types | Reading a route's types |
InferRouteParams, RouteParamsFromKey | questpie/types | Reading a route's URL params |
RouteMeta, RouteMcpMeta, RouteAccess | questpie/services | Typing 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>;| Helper | On a JSON route | On a raw route |
|---|---|---|
InferRouteInput | The .schema() type | never |
InferRouteOutput | The output or handler return | Response |
InferRouteParams | Whatever .params<…>() said | Same |
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.
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" }| Argument | executeJsonRoute | executeRawRoute |
|---|---|---|
| 1 | the app | the app |
| 2 | the route definition | the route definition |
| 3 | the input | a Request |
| 4 | a RequestContext | a RequestContext |
| 5 | a Request | the URL params |
| 6 | the URL params | none |
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);| Field | Example |
|---|---|
key | posts/[id] |
pattern | posts/:id |
path | /posts/:id |
params | ["id"] |
methods | ["GET"] |
mode | "json" or "raw" |
meta | your .meta() object |
definition | the route itself |
methods is always an array, even for the single-method route that one file
produces.
Related
- Client SDK, where these same types land for the caller.
- Codegen, what erases the handler and why the helpers are built to survive it.
Metadata
One .meta() call names a route for the OpenAPI spec and can hand the same route to an agent as an MCP tool. It changes nothing about how the route runs.
Lifecycles
What "singleton" and "request" actually do, when each instance is built and thrown away, and the two ways an async create can fail on you.