Fast Phase Query Parameters
Design Log #117: Fast-Phase Query Parameters
Written for AI agents. See Log Methodology Note below for details.
Background
Jay Stack supports URL path parameters via file-system routing ([slug], [[optional]], [...catchAll]). These are extracted by the route scanner, merged with inferredParams (DL#113), and passed as props to both slow and fast render phases.
URL query parameters (?page=2&sort=price) have no dedicated support. The raw URL string is available in PageProps.url but never parsed. Components wanting query params must parse the URL themselves.
Current data flow
Request: GET /products/vases?page=2&sort=price
dev-server.ts:
pageParams = { slug: 'vases' } ← path params (from Express + inferredParams)
pageProps = { language: 'en', url: '/products/vases?page=2&sort=price' }
slowly-changing-runner.ts:
props = { ...pageProps, ...pageParams } ← slow phase sees raw url only
fast-changing-runner.ts:
props = { ...pageProps, ...pageParams } ← fast phase sees raw url only
Problem
- No ergonomic way to access query parameters — component authors must manually parse
props.url - No phase-appropriate boundary — query params are per-request data, but there's no type-level distinction between slow (cached/SSG) and fast (per-request) data sources
Why query params don't belong in the slow phase
- Slow render results are cached per route + path params (
SlowRenderCacheinslow-render-cache.ts) - Query params change frequently (
?page=1,?page=2,?sort=name, etc.) - Including query params in slow phase would either bust the cache on every distinct query string, or serve stale cached content
- Conceptually, slow phase is build-time/SSG — query params don't exist at build time
Why query params belong in the fast phase
- Fast phase runs per-request (SSR)
- Query params modify how a resource is displayed (pagination, sorting, filtering), not which resource
- Fast phase already receives per-request data (
pageProps,pageParams)
Design
New type: RequestQuery
// jay-stack-types.ts
export interface RequestQuery {
query: Record<string, string>;
}
Type-safe fast-only access
Change RenderFast to intersect PropsT with RequestQuery:
// Before
export type RenderFast<Services, PropsT, FastViewState, FastCarryForward> = (
props: PropsT,
...services: Services
) => Promise<FastRenderResult<FastViewState, FastCarryForward>>;
// After
export type RenderFast<Services, PropsT, FastViewState, FastCarryForward> = (
props: PropsT & RequestQuery,
...services: Services
) => Promise<FastRenderResult<FastViewState, FastCarryForward>>;
RenderSlowly stays unchanged — props: PropsT has no query field.
Result
// ✅ Fast phase — query is available
.withFastRender(async (props, carryForward, dbService) => {
const page = parseInt(props.query.page || '1');
const sort = props.query.sort || 'name';
const products = await dbService.getProducts({ page, sort });
// ...
})
// ❌ Slow phase — query does NOT exist on props (type error)
.withSlowlyRender(async (props, dbService) => {
props.query // ← TypeScript error: Property 'query' does not exist
// ...
})
Runtime changes
Parse query params from the request in dev-server.ts and pass to renderFastChangingData:
// dev-server.ts — inside mkRoute handler
const urlObj = new URL(req.originalUrl, `http://${req.headers.host}`);
const query: Record<string, string> = {};
for (const [key, value] of urlObj.searchParams) {
query[key] = value; // last value wins for repeated keys
}
Add query parameter to renderFastChangingData and merge into fast props:
// fast-changing-runner.ts
export async function renderFastChangingData(
pageParams: object,
pageProps: PageProps,
carryForward: object,
parts: Array<DevServerPagePart>,
instancePhaseData?: InstancePhaseData,
forEachInstances?: ForEachHeadlessInstance[],
headlessInstanceComponents?: HeadlessInstanceComponent[],
mergedSlowViewState?: object,
query?: Record<string, string>, // ← new
): Promise<AnyFastRenderResult> {
// ...
const partProps = {
...pageProps,
...pageParams,
query: query || {}, // ← inject into fast props
...(contractInfo && { ... }),
};
// ...
}
Interactive phase
No framework API needed. Client-side code reads query params via standard browser APIs:
const params = new URLSearchParams(window.location.search);
const page = params.get('page');
If a component needs reactive query params, it can create a signal from window.location.search in the interactive phase.
Multi-value query params
?tag=a&tag=b — URLSearchParams iteration yields both entries, but Record<string, string> stores only one. The last value wins (consistent with Express req.query simple mode).
Multi-value support (Record<string, string | string[]>) can be added later if needed. This keeps the initial API simple.
Implementation Plan
Phase 1: Type changes
File: packages/jay-stack/full-stack-component/lib/jay-stack-types.ts
- Add
RequestQueryinterface - Change
RenderFastprops type fromPropsTtoPropsT & RequestQuery - Export
RequestQueryfrom index
Phase 2: Runtime — parse and pass query params
File: packages/jay-stack/dev-server/lib/dev-server.ts
- In
mkRoutehandler: parsereq.originalUrlintoRecord<string, string> - Pass
querytohandlePreRenderRequest,handleCachedRequest,handleClientOnlyRequest - Each handler passes
querytorenderFastChangingData
File: packages/jay-stack/stack-server-runtime/lib/fast-changing-runner.ts
- Add
queryparameter torenderFastChangingData - Merge
queryintopartPropsfor page-level fast render - Merge
queryinto props for instance fast render
Phase 3: Tests
File: packages/jay-stack/stack-server-runtime/test/fast-changing-runner.test.ts (new or extend existing)
- Test that query params appear in fast render props
- Test that empty query params default to
{} - Test that last-value-wins for repeated query keys
File: packages/jay-stack/full-stack-component/test/jay-stack-builder.test.ts (extend)
- Type-level test:
RenderFastcallback receivesqueryin props - Type-level test:
RenderSlowlycallback does NOT havequeryin props
Phase 4: Update headless instance fast render
In fast-changing-runner.ts, instance fast render also needs query params:
- Static instances — add
queryto instance props - ForEach instances — add
queryto forEach item props
Examples
✅ Paginated product list
export const page = makeJayStackComponent<ProductListContract>()
.withServices(PRODUCTS_DB)
.withSlowlyRender(async (props, db) => {
// Slow: fetch categories (cached, no query params)
const categories = await db.getCategories();
return phaseOutput({ categories }, {});
})
.withFastRender(async (props, carryForward, db) => {
// Fast: paginate based on query params (per-request)
const page = parseInt(props.query.page || '1');
const sort = props.query.sort || 'name';
const products = await db.getProducts({ page, sort });
return phaseOutput({ products, currentPage: page, sortBy: sort }, {});
})
.withInteractive((refs, viewState) => {
/* ... */
});
✅ Search page
.withFastRender(async (props, carryForward, searchService) => {
const q = props.query.q || '';
const results = q ? await searchService.search(q) : [];
return phaseOutput({ searchQuery: q, results }, {});
})
❌ Anti-pattern: query params in slow phase
.withSlowlyRender(async (props, db) => {
const page = props.query.page; // ← TypeScript error!
// query params are per-request, slow phase is cached — this is wrong
})
Trade-offs
| Aspect | Benefit | Cost |
|---|---|---|
| Type safety | Slow phase cannot access query (compile error) | Slightly different prop types between phases |
| Simplicity | Record<string, string> is easy to use |
No multi-value support initially |
| Caching | Slow cache unaffected by query variations | None |
| Backward compat | Existing components unaffected (query is additive) | Fast render callbacks see new field |
Verification Criteria
props.queryis available inwithFastRendercallbacks with correct typesprops.queryis NOT available inwithSlowlyRendercallbacks (TypeScript error)- Query params from request URL are correctly parsed and passed to fast phase
- Empty query string →
queryis{} - Repeated keys (
?a=1&a=2) → last value wins ({ a: '2' }) - Slow render cache is unaffected (keyed by path params only)
- Headless instance fast render also receives query params
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.