This page is part of a static HTML representation of the TiddlyWiki at https://mws.tiddlywiki.com/

Route Definers

13th May 2025 at 11:13pm

Basic Route Definer

The original route definer which I used to type TiddlyWiki server routes, but the types ended up being extremely complicated for what I actually ended up needing. It is still used internally to actually define the routes, but the JavaScript side of it is very simple.

method
A subset of the Allowed Methods.
path
A regex starting with ^/ which matches the request. The first route handler which matches is used. Routes may be nested, and the full match is removed from the URL before matching children. If a parent route matches, it will be called, even if it has no child matches.
denyFinal
If this route matches, but none of its children do, the server will return 404 NOT FOUND. Otherwise, its state handler will be called and expected to handle the request, even if none of its children match.
pathParams
An array of key names for regex match groups for the pathParams object.
bodyFormat
The Body Format which the request wishes to receive. If the method is only GET and HEAD, this is ignored, as no request body is expected. Internally, the request is probably drained early, just in case a body was sent.
handler - a separate callback argument
If the route matches, the handler is called. The handler is called at each level in order, so parents may add additional (out of type) properties to the state object or handle some requests and allow others to go through to the matched child.

Match result

The StateObject has a routePath parameter containing the "path" through the "tree" of route definitions. In other words, it has the first matched route, and then the first matched child of that route, and then the first matched child of that route, and so on.

It is an array of objects with the following properties.

route
an object containing the options for the route listed above
params
an array of the match groups (match.slice(1))
remainingPath
The remaining URL to match (if this is zero-length, it will be a /)

Zod Route Definers

The rest of the route definers are used by creating a class with the route definitions as properties, and then calling that class on server startup to register the routes.

class RoutesClass { test = zodManage(z => z.any(), async e => null) }
const RoutesKeyMap: RouterKeyMap<RoutesClass, true> = { test: true }
registerZodRoutes(root, new RoutesClass(), Object.keys(RoutesKeyMap));

RoutesKeyMap would have the keys of all the routes in the class, and the type makes sure no routes have been missed, while also allowing the class to have extra properties that are not routes.

zodRoute

The zodRoute function creates type-safe route definitions with Zod validation. It takes a single configuration object with the following properties:

method: string[]
An array of HTTP methods (e.g., ["GET", "POST"]). Must be a subset of allowed methods.
path: string
A slash-separated string path with optional parameters prefixed with : (e.g., "/recipes/:recipe_name/tiddlers/:title").
bodyFormat: BodyFormat
The expected body format: "ignore", "string", "json", "buffer", "www-form-urlencoded", "www-form-urlencoded-urlsearchparams", or "stream". For GET and HEAD requests, this is always treated as "ignore".
zodPathParams: (z: Z2<"STRING">) => Record<string, ZodType>
A function that returns an object defining Zod validations for path parameters. The keys must match the parameter names in the path. If validation fails, returns 404.
zodQueryParams?: (z: Z2<"STRING">) => Record<string, ZodType>
Optional function defining Zod validations for query parameters. Query params are arrays of strings by default.
zodRequestBody?: (z: Z2<BodyFormat>) => ZodType
Optional function defining Zod validation for the request body. Only valid for "string", "json", and "www-form-urlencoded" body formats. If validation fails, returns 400.
securityChecks?: { requestedWithHeader?: boolean }
Optional security checks. If requestedWithHeader is true, requires the x-requested-with: fetch header for non-GET/HEAD/OPTIONS requests.
corsRequest?: (state: ZodState<"OPTIONS", "ignore", P, Q, ZodUndefined>) => Promise<symbol>
Optional CORS preflight handler for OPTIONS requests. Cannot authenticate but can provide endpoint information.
inner: (state: ZodState<Method, BodyFormat, PathParams, QueryParams, RequestBody>) => Promise<JsonValue>
The main route handler that receives a fully validated and typed state object.

Example

const getUser = zodRoute({
  method: ["GET"],
  path: "/users/:user_id",
  bodyFormat: "ignore",
  zodPathParams: z => ({
    user_id: z.string().uuid()
  }),
  zodQueryParams: z => ({
    include_roles: z.enum(["yes", "no"]).array().optional()
  }),
  inner: async (state) => {
    const { user_id } = state.pathParams; // typed as { user_id: string }
    const { include_roles } = state.queryParams; // typed as { include_roles?: ("yes"|"no")[] }
    
    return await getUserById(user_id, include_roles?.[0] === "yes");
  }
});

admin Helper Function

The admin function is a convenience wrapper around zodRoute specifically for admin API endpoints. It automatically sets up:

  • Method: ["POST"]
  • Path: "/admin/$key" (where $key is replaced with the property name)
  • Body format: "json"
  • Security: Requires x-requested-with: fetch header
  • Database transactions: Automatically wraps the handler in a Prisma transaction
  • Authentication: Provides access to authenticated user state

Signature

function admin<T extends ZodTypeAny, R extends JsonValue>(
  zodRequest: (z: Z2<"JSON">) => T,
  inner: (state: ZodState<"POST", "json", {}, {}, T>, prisma: PrismaTxnClient) => Promise<R>
): ZodRoute<"POST", "json", {}, {}, T, R>

Parameters

zodRequest: (z: Z2<"JSON">) => ZodType
Function defining the expected shape of the JSON request body.
inner: (state, prisma) => Promise<JsonValue>
Handler function that receives the validated state and a Prisma transaction client.

Example

const user_create = admin(z => z.object({
  username: z.string().min(3),
  email: z.string().email(),
  role_id: z.string().uuid()
}), async (state, prisma) => {
  // state.data is typed based on the zodRequest schema
  const { username, email, role_id } = state.data;
  
  // Create user within the automatic transaction
  const user = await prisma.users.create({
    data: { username, email, role_id }
  });
  
  return { user_id: user.user_id, username, email };
});

registerZodRoutes Function

This function registers multiple Zod routes from a class instance to a parent route. It's the bridge between route definitions and the actual router.

Signature

function registerZodRoutes(
  parent: ServerRoute,
  router: object,
  keys: string[]
): void

Parameters

parent: ServerRoute
The parent route to register child routes under.
router: object
An instance of a class containing route definitions as properties.
keys: string[]
Array of property names to register as routes. Usually Object.keys(RouterKeyMap).

Usage Pattern

export class UserManager {
  static defineRoutes(root: ServerRoute) {
    registerZodRoutes(root, new UserManager(), Object.keys(UserKeyMap));
  }

  user_create = admin(z => z.object({
    username: z.string(),
    email: z.string().email()
  }), async (state, prisma) => {
    // Implementation
  });

  user_list = admin(z => z.undefined(), async (state, prisma) => {
    // Implementation  
  });
}

export const UserKeyMap: RouterKeyMap<UserManager, true> = {
  user_create: true,
  user_list: true,
};

// Register during server startup
serverEvents.on("mws.routes", (root) => {
  UserManager.defineRoutes(root);
});