@rapidrest/react
v1.0.1
Published
A library for serving React based content from a RapidREST server.
Readme
RapidREST: React Library
A file-based React page framework for @rapidrest/service-core
servers. Drop .tsx files into an app/ directory and @rapidrest/react turns them into
server-rendered routes — with layouts, DI-powered data fetching, Redis-backed page caching, and
opt-in client-side hydration — all served from the same RapidREST process, no separate Node
front-end server required.
For complete documentation please visit RapidREST.dev.
Features
File-Based Page Routing
- Convention-based routing from an
app/directory (app/pets.tsxandapp/pets/index.tsxboth serveGET /pets) app/_layout.tsx— a single global HTML wrapper applied to every page- Default error page rendering. (e.g.
app/_404.tsx,app/_500.tsx) - Mount the router at any prefix (
@Route("/app/*"),@Route("/*"), etc.) — page resolution is prefix-agnostic
Server-Side Rendering
- Renders pages to HTML with
react-dom/serveron every request; no client JS is required unless hydration is explicitly enabled - Three composable levels of props for server-side rendering. page → service → route: a page's own
exported
fetchProps, a DI@ReactServicefor that path, and afetchProps()override on the route subclass
Dependency Injection
- Subclass
ReactRouteand use@Injectto pull RapidREST services intofetchProps() @ReactService(path)binds a plain DI-managed class as the data source for one or more page paths, so page components stay framework-free
Caching
- Built-in full-page cache with a configurable TTL per route
- Render cache supports multi-instance deployments using a common Redis database
Opt-In Client Hydration
- Pages are SSR-only by default; set
hydrate = trueon a route to hydrate specific pages on the client withreact-dom/client createViteConfig()auto-discovers page entry points from yourapp/directory and generates virtual hydration modules for each — no hand-written entry files- Built JS/CSS bundles are resolved from Vite's manifest and injected automatically, and served directly by the route at request time
- Serialized props are embedded in the page (XSS-safely escaped) and read back on the client via
hydrateRoute()/getHydrationProps()
Developer Experience
rapidreact dev— runs your server with live restarts (vianodemon, falling back totsx --watch) alongsidevite build --watchfor the client bundle, in one commandrapidreact build— compiles the server withtscand bundles the client withvite buildfor productionrapidreact export— crawls every app page and writes a plain HTML/CSS/JS static site to disk, deployable to any static host with no server required at request time- Server-Sent Events live-reload: connected browsers automatically refresh after a dev rebuild — no browser extension or separate dev server needed
- Per-user field allow-listing (
userFields) — control exactly whichreq.userfields (if any) are exposed to page props and the hydration payload
Installation
NPM
npm i @rapidrest/reactYarn
yarn add @rapidrest/reactQuick Start
app/
_layout.tsx # global HTML wrapper (required)
_404.tsx # optional 404 page
_500.tsx # optional error page
index.tsx # GET /
pets.tsx # GET /petsMount the router by subclassing ReactRoute:
// src/routes/AppRouter.ts
import { ReactRoute } from "@rapidrest/react";
import { RouteDecorators } from "@rapidrest/service-core";
const { Route } = RouteDecorators;
@Route("/*")
export class AppRouter extends ReactRoute {
protected readonly appDir: string = "app";
}A page component, with page-level data fetching:
// app/pets.tsx
import React from "react";
export default function Pets({ pets }: { pets: Pet[] }) {
return (
<ul>
{pets.map((pet) => <li key={pet.id}>{pet.name}</li>)}
</ul>
);
}
export async function fetchProps() {
return { pets: await fetch("https://api.example.com/pets").then((r) => r.json()) };
}Or fetch the same data through dependency injection with @ReactService, so pages stay
framework-free and services can use @Inject like any other RapidREST class:
// src/services/PetsService.ts
import { ReactService } from "@rapidrest/react";
import { RepoUtils } from "@rapidrest/service-core";
import { ObjectDecorators } from "@rapidrest/core";
import Pet from "../models/Pet.js";
const { Inject } = ObjectDecorators;
@ReactService("/pets")
export default class PetsService {
@Inject(RepoUtils, { name: Pet.name, args: [Pet] })
private petRepo?: RepoUtils<Pet>;
public async fetchProps() {
return { pets: await this.petRepo?.find({}) };
}
}Client Hydration (optional)
Enable hydration on a route, and generate a matching Vite build:
export class AppRouter extends ReactRoute {
protected readonly appDir: string = "app";
protected readonly hydrate: boolean = true;
}// vite.config.ts
import { createViteConfig } from "@rapidrest/react/vite";
export default createViteConfig({ appDir: "app" });Point the route at the generated manifest via nconf (react:manifestPath, e.g.
dist/public/.vite/manifest.json) and it will inject the right <script>/<link> tags and
serve the built assets automatically.
Multiple Apps (optional)
A project can run more than one React app side by side — e.g. a public www app at / and an
admin app at /admin — each its own ReactRoute subclass with its own appDir:
@Route("/")
export class WwwRoute extends ReactRoute {
protected readonly appDir: string = "apps/www";
}
@Route("/admin")
export class AdminRoute extends ReactRoute {
protected readonly appDir: string = "apps/admin";
}createViteConfig() accepts appDir as an array to build every app's hydration entries into one
manifest:
// vite.config.ts
export default createViteConfig({ appDir: ["apps/www", "apps/admin"] });Static Export
Every page under app/ is already file-enumerable — there are no dynamic/parameterized routes
(e.g. pets/[id].tsx) — so a whole @rapidrest/react app can be crawled once and exported as a
plain static site: HTML, CSS and JS, deployable to any static host (S3, Netlify, GitHub Pages, a
CDN) with no server needed at request time.
Rather than reimplementing ReactRoute's rendering logic, export boots your real server (real
DI, real config, real @ReactServices) and crawls it over real HTTP, so the exported output can
never diverge from what a live deployment actually serves.
Write a small export entry script — the static-export analog of your src/server.ts — using
runStaticExport():
// src/export.ts
import { Logger } from "@rapidrest/core";
import { ObjectFactory } from "@rapidrest/service-core";
import { runStaticExport } from "@rapidrest/react";
import config from "./config.js";
const logger = Logger();
const objectFactory = new ObjectFactory(config, logger);
const result = await runStaticExport(
{ config, basePath: ".", logger, objectFactory },
{ appDir: "app", routePrefix: "/app", outDir: "dist/export" }
);
await objectFactory.destroy();
if (result.errors.length > 0) {
console.error(`[export] Completed with ${result.errors.length} error(s).`);
process.exit(1);
}
console.log(`[export] Wrote ${result.pages.length} page(s) to dist/export.`);For a multi-app project, pass apps instead of appDir/routePrefix
— each app's own routePrefix also becomes its output subdirectory, so pages from different apps
can't collide in dist/export:
const result = await runStaticExport(
{ config, basePath: ".", logger, objectFactory },
{
outDir: "dist/export",
apps: [
{ appDir: "apps/www", routePrefix: "" },
{ appDir: "apps/admin", routePrefix: "/admin" },
],
}
);
// -> dist/export/index.html, dist/export/admin/index.html, ...Then run:
rapidreact exportThis builds the client bundle (vite build) and runs src/export.ts (or src/export.tsx) with
tsx under NODE_ENV=production, writing dist/export/index.html,
dist/export/pets/index.html, etc. (trailing-slash/index.html convention — works with any
static file server), plus dist/export/404.html for static-host fallback routing, and a copy of
dist/public (the built hydration assets) into the export root.
Known limitations:
- Route discovery uses the same file convention as hydration entry points (top-level
.tsx+ nestedindex.tsx). A page served from an unconventional nested non-index file won't be auto-discovered — pass it explicitly viapaths. - Hydration asset URLs are always root-absolute, so the exported site only works correctly when
served from
/— the same pre-existing constrainthydratealready has in a live deployment. - Props are frozen at export time (like Next.js's static export): pages whose
fetchProps/@ReactServicedepend on per-request or authenticated state will bake in whatever an unauthenticated crawl request renders. Useexcludeto skip personalized pages entirely. - There is no support for dynamic/parameterized routes (
pets/[id].tsx) —ReactRoutedoesn't have that concept today, so there's nothing to enumerate.
Requirements
This package targets Node.js >=24.0.0 and is published as an ESM-only package.
It declares @rapidrest/core, @rapidrest/service-core, react and react-dom as required peer
dependencies. The remaining peer dependencies are optional and only need to be installed if you
use the corresponding feature:
| Peer dependency | Required for |
| ----------------------- | ------------------------------------------------------ |
| @rapidrest/core | Always |
| @rapidrest/service-core | Always |
| react | Always |
| react-dom | Always |
| vite | Client hydration builds (createViteConfig, rapidreact build/dev) |
| @vitejs/plugin-react | Client hydration builds |
| tsx | rapidreact dev server watcher (used directly, or via nodemon --exec) |
| nodemon | rapidreact dev — preferred server watcher when installed |
License
MPL v2.0 — see LICENSE.
