Skip to main content

Data Fetching

A page rendered entirely on the server needs its data before it can render; there's no client-side fetch happening afterward the way a single-page app might do it. @rapidrest/react gives you three different places to supply that data, depending on how simple the need is or how deeply it has to reach into RapidREST's own dependency injection, and merges whatever each one returns into the page's props on every request:

props = { user, userUid, ...pageProps, ...serviceProps, ...routeProps }

Later sources win on overlapping keys. A route-class override can always override a service, which can always override the page itself. user/userUid come from the authenticated request, if any, before either of the sources below run.

Controlling which user fields are exposed

userUid (just req.user?.uid) is always passed to page props when a request is authenticated. The full user prop, however, is allow-listed: ReactRoute won't expose any req.user fields as props.user unless you opt in on your route subclass with userFields:

@Route('/app')
export class AppRouter extends ReactRoute {
protected override readonly userFields: string[] | null = ['uid', 'email'];
}
PropertyTypeDefaultWhat it controls
userFieldsstring[] | nullnullNames of req.user fields to copy onto props.user. null means no user prop is exposed at all — only the scalar userUid.

With userFields left at its default null, pages and the hydration payload only ever see userUid, not the rest of req.user — useful when a page just needs to know who is logged in without leaking the full user record (roles, hashed credentials, etc.) into client-visible props. Set userFields to the specific field names a page actually needs (e.g. ['uid', 'email', 'displayName']); fields not present on req.user are silently skipped rather than showing up as undefined.

Because userFields is a property on the route class, it applies to every page served by that route — there's no per-page override.

Page-level fetchProps

The simplest option: export an async fetchProps function from the page file itself.

// app/pets.tsx
import type {HttpRequest} from '@rapidrest/service-core';

export async function fetchProps(req: HttpRequest) {
return {pets: await fetch('/api/pets').then((r) => r.json())};
}

export default function PetsPage({pets}: {pets: string[]}) {
return (
<ul>
{pets.map((p) => <li key={p}>{p}</li>)}
</ul>
);
}

This is enough for most pages, and keeps the data-fetching code next to the component that uses it.

Injecting server-side services with @ReactService

For data that needs RapidREST's own dependency injection (a database-backed service, a cache client, anything you'd normally @Inject into a route), register a service class against a specific page path with @ReactService:

// src/services/PetService.ts
import {ReactDecorators} from '@rapidrest/react';
import type {HttpRequest} from '@rapidrest/service-core';

const {ReactService} = ReactDecorators;

@ReactService('/app/pets')
export class PetService {
async fetchProps(req: HttpRequest) {
return {pets: await this.petRepo.findAll()};
}
}

Because this is a plain class discovered by auto-discovery, you can @Inject a repository, another service, or config into it exactly as you would in a route class. RapidREST instantiates it once and calls its fetchProps(req) for every request to the matching path, merging the result in after the page's own fetchProps.

Overriding at the route-class level

The route class mounting your app (the one extending ReactRoute) can also override fetchProps directly. This is the last, most authoritative layer, and a good place for cross-cutting data every page needs:

@Route('/app')
export class AppRouter extends ReactRoute {
protected override async fetchProps(req: HttpRequest) {
return {featureFlags: await this.flagsService.getAll()};
}
}

Return {} for paths you don't want to affect, so page- and service-level props for those paths pass through untouched.