Data Flow
Frontend data in Vext starts on the server. Route handlers call services, prepare JSON-safe values, and pass them into res.render().
Following Full-Stack Quick Start, this page uses the same /dashboard route for first-screen data, navigation, and local loading. Complete the Layouts and Components example before adding layout data. Add business authentication using Authentication and Security.
Read by task: First-screen Data → GET Navigation Example → Navigation API Reference → POST Write Form. The generated browser entry configures the navigation runtime; application components use the public interfaces below.
First-screen Data
Create these two files. The example uses an in-memory summary so it can be verified directly. In real business code, call a registered service from the handler and select the fields returned to the browser.
After the app registers auth(), auth: true protects the route while req.auth carries the framework identity and claims. A user profile is application data: load it through your own service instead of assuming that Vext injects req.user.
The page receives the same serializable data during SSR and hydration:
Layout Data
Put shell data such as navigation, user menus, and workspace information in options.layoutData. This replaces the render call in the admin/dashboard handler from the completed layout example. The layout IDs are root . and admin:
Layouts consume the object under their ID through props.data; they do not import services directly. Query real profile data by req.auth.userId in an authenticated handler. Navigation visibility affects UI only; the server must check permissions independently.
Locale Messages
Page copy comes from src/frontend/locales/** and optional render messages. With frontend i18n enabled, a handler may explicitly supply the messages for this render. They replace the current messages object rather than recursively patching it. This can replace the dashboard render call above:
This component can read the messages in a page or layout. Its generic parameter is only a TypeScript declaration; configuration or render options must actually provide the messages. See Frontend i18n for complete type generation and fallback rules.
Same-route Navigation
After hydration, Vext can request the same document route as a versioned page result. There is no second loader or action registration API: the route handler and its middleware, auth/session, CSRF, validation, cache, timeout, redirect, and error behavior remain authoritative.
The stable surface is Link, Form, navigate, prefetch, revalidate, useNavigation, useFetcher, and useRouteData.
Keep the route above unchanged and replace its dashboard page with this interactive version. Links and forms still request /dashboard; the GET form demonstrates a query without requiring another mutation endpoint.
useRouteData() may return undefined on the server, where a browser runtime is not configured. Falling back to page props keeps first-screen content. Link accepts prefetch="none" | "click" | "visible" and defaults to "click". Form retains a native form; this GET example still submits a document request with JavaScript disabled. A mutation form needs a handler for its method and must meet its auth, CSRF, and validation requirements. useFetcher() uses the same route without changing browser history.
Navigation API Reference
Import these interfaces from vextjs/frontend. Components still produce native HTML during SSR; call programmatic navigation only in a browser initialized by the generated entry.
Optional parameters differ by entry point:
A successful page result from fetcher.submit updates that fetcher's data without replacing the entire page. Non-GET submissions also revalidate the current page, including failures returning undefined. Ordinary local loading does not change the address, but a redirect result starts page navigation and can change it. For a local submission that should stay on the page, return a page result with res.render() instead of a redirect.
Read page errors through useNavigation().error or fetcher.error. VextPageResultError is exported from vextjs/frontend in both package and generated browser runtime, using the same class as navigation, so instanceof works and exposes status, optional code, and requestId. Network errors may still be ordinary Error instances; guard those separately.
Do not rely only on catching the returned Promise to identify success. Navigation errors are recorded in the snapshot, while fetcher errors usually return undefined. A 401/403 or incompatible protocol/build requires document navigation, so the error may not remain visible in the current component. Programmatic APIs and fetcher methods throw without a runtime; Link/Form retain native HTML behavior.
POST Write Form
Create the following two new files in the full-stack template and merge the configuration into src/config/default.ts, retaining existing frontend settings. The example stores a display name per Session, with no database or identity authentication; real account updates also need separate authentication and authorization. Global Session must precede global CSRF; route-only Session does not provide that ordering.
After obtaining the token in GET, save Session before placing the token in a hidden field. The full-stack template permits streaming SSR by default, and generating the token mutates Session; do not wait until streaming starts to save it. Browser submission also carries the associated Session Cookie. Form defaults to POST; the example has no hidden action registration or automatic token injection. Token responses must stay private and out of shared caches: this example disables route cache explicitly, and req.csrfToken() also sets no-store. See Cookies and Sessions for storage, production Cookies, and multi-instance configuration.
Run npm run build -- --typecheck, then npm start -- --port 3000, and open /preferences. Verify in order:
To verify rejection with an HTTP client, first GET the page and retain the same Cookie, then send form-urlencoded label and _csrf to /preferences. A token without its session also fails. Do not disable production CSRF to reproduce success. Stop the server afterward; default in-memory Session is not shared across restarts or Workers.
Navigation Lifecycle
useNavigation() reports idle, loading, submitting, revalidating, error, or aborted. A revalidation keeps the last-known-good page visible until the replacement commits. A newer navigation aborts the older request, equivalent GET requests are deduplicated, and revalidate({ routeId, path, tags, keys }) can invalidate matching entries within the current locale and auth/session partition.
The browser requests application/vnd.vext.page+json;v=1 only for enhanced navigation. Protocol, build id, permission, decode, or route-asset incompatibility falls back to exactly one document navigation. This envelope is an internal runtime protocol, not a user-implemented RPC format.
Client API Calls and Cache Boundary
Use the generated typed API client or plain fetch for JSON API calls that are not page navigation. First-screen and page-navigation data should normally flow through res.render(). Vext's browser cache is partitioned by route, normalized URL, locale, auth/session identity, protocol, and contract digest; authenticated or no-store page results are not stored in the shared public cache.
Verify Data Flow
Run npm run build and start npm start -- --port 3000. Direct access to /dashboard should show Dashboard: 42; ?view=compact should show Compact: 42, including in the response source. With the interactive version, the GET form should switch to the compact summary; local loading should update its own result without changing the address; refresh should retain the current page until a new result arrives. Disable JavaScript and submit the GET form again to confirm a full page can still open. Stop the service afterward. See Render Data and Cache for cache policy and API Client and Contracts for JSON APIs.