Architecture
03. From routes to requests
How route definitions become request destinations and validated page parameters.
A destination combines a Page with its full path, middleware, and surrounding Layout/Loading scopes. Effront assembles destinations before serving requests, separating invalid declarations from URLs that match a route but fail Page parameter validation.
Start with the destination the server receives
Routes is a tree of Pages and mounted child Routes, not a live HTTP matcher. During EFFRONT.make, compileRouteGraph in application/route-graph.ts flattens it into destinations:
packages/core/src/application/route-graph.tsexport type RouteScope<Services> = {
readonly id: string;
readonly layout: LayoutComponent<Services> | null;
readonly loading: LoadingComponent<Services> | null;
};
export type CompiledDestination<Services> = {
readonly middleware: ReadonlyArray<AnyMiddleware<Services>>;
readonly page: PageImplementationState<Services>;
readonly pattern: AbsolutePath;
readonly scopes: ReadonlyArray<RouteScope<Services>>;
};
export type CompiledRouteGraph<Services> = readonly [
CompiledDestination<Services>,
...Array<CompiledDestination<Services>>,
];Mounting a child Page at /:id beneath /items produces /items/:id. The traversal inherits the parent's scopes and adds one only when the child Routes defines Layout or Loading. Grouping paths alone creates no rendering boundary. Each scope ID combines the declaration's scopeId with its mounted prefix, distinguishing repeated mounts.
packages/core/src/application/route-graph.ts for (const route of currentState.pages) {
const pattern = joinRoutePaths(prefix, route.path);
validateUnreservedPath(pattern);
destinations.push(
Object.freeze({ middleware, page: getPageState(route.page), pattern, scopes }),
);
}
for (const mount of currentState.mounts) {
visit(mount.routes, joinRoutePaths(prefix, mount.path), scopes, middleware);
}
};The loop visits the current node's Pages, then its mounts, each in registration order. Interleaved page and mount calls therefore do not retain a combined order. This is traversal order, not matching priority: Effect HTTP owns matching, and this compiler does not sort static and dynamic paths.
resolveRouteMiddleware retains the inherited chain and appends the current declaration's suffix after their shared prefix. Remaining duplicates throw TypeError. The compiler also requires a root Layout and at least one Page, then returns a frozen, nonempty destination array. Requests use that array without traversing Routes again.
Connect URL captures to Page input
In application/routes.ts, page and mount return new definitions rather than mutating existing ones. RoutesDefinition tracks Layout presence, registered paths, and matching shapes so later additions can be checked against earlier ones.
MatchingPageParams compares URL parameter names with the Page Schema's Encoded keys. For /items/:id, the Schema must have exactly the encoded key id, while rendering receives the decoded Type. A transformation can change the value or output key without changing the URL parameter name. Static paths require a Page without a parameter Schema.
Runtime registration checks syntax, application identity, and agreement between the presence of parameters and a Schema. It does not repeat the type-level comparison of all encoded keys.
ValidRoutePath and analyzeRoutePath in application/route-path.ts pair type-level and runtime grammar checks:
- Paths start with
/, use named segments such as:id, and may end with a named catch-all such as*path. - Except for root
/, empty segments and trailing slashes are invalid. Dot segments, repeated parameter names, query strings, and other wildcard forms are also invalid. - Mount prefixes must be static. Child Routes must be nonempty and share the application identity.
Locate failures during application assembly
Collision checks lowercase static segments and remove parameter names to compare matching shapes. They reject equivalent shapes, not every overlap:
/items/:idand/items/:slugcollide./Aboutand/aboutcollide./manual/*pathalso reserves/manual, where its capture is empty./items/newand/items/:idhave different shapes and can coexist.
Mounts check child paths after joining the prefix, so they can collide with Pages registered directly on the parent. joinRoutePaths treats / specially to avoid an extra slash. Runtime collision checks throw TypeError, complementing the type checks.
validateUnreservedPath checks final joined paths to protect /_effront. It rejects a first segment equal to _effront in any case, or a dynamic first segment that could capture it. Thus /:slug is invalid as a final application path, while /items/:slug passes this check. These failures occur during assembly, before an incoming URL is matched.
Separate route matching from parameter validation
makeRouteLayer in server/application.ts registers GET and POST handlers through HttpRouter.add. Effect HTTP matches URLs and decodes captures. Effront renames its * capture to the declared catch-all name, using an empty string if absent. Rendering then reads HttpRouter.params and applies the Page Schema:
packages/core/src/server/application.ts if (request.method !== "POST" && destination.page.paramsSchema !== null) {
return yield* Schema.decodeEffect(destination.page.paramsSchema)(encodedParams).pipe(
Effect.matchEffect({
onFailure: () =>
Effect.succeed(
HttpServerResponse.empty({ status: 404, headers: DynamicResponseHeaders }),
),
onSuccess: (value) => renderResponse({ _tag: "Decoded", value }),
}),
);
}
return yield* renderResponse({ _tag: "Encoded", value: encodedParams });
});For non-POST requests, a typed Schema decoding failure returns an empty 404 with private, no-store. Success passes Decoded parameters to the Page so it does not decode twice. For example, /items/not-a-number can match /items/:id but fail a Schema that decodes the ID to a number.
Unmatched routes and failures reading URL captures occur outside this Schema handler. POST also bypasses it: rendering receives Encoded parameters, and the Page component decodes them. The GET Schema-to-404 rule therefore does not describe Server Function input or refresh failures.
Request processing follows the selected destination's Context and lifetime. Rendering turns its scopes into UI, while Server Functions follows the POST path.