Docs/Routing & Data/Server Data Loaders

Routing & Data

Server Data Loaders

Server Data Loaders — Routing & Data documentation for BCP Framework.

Last updated: 2026-08-29T01:11:41

Server data loaders

BCP 0.1.9 adds route-level server data loading for pages. A loader lives next to a page and runs on the server before that page is rendered. Loader-backed routes also participate in BCP client navigation: a navigation request executes the target loader on the server, returns fresh loader data, and renders the next route without requiring a full document reload.

File convention

text
app/
└─ users/
   └─ [id]/
      ├─ loader.ts
      └─ page.tsx

loader.ts must export a named loader() function:

TypeScript
import {
    getSession,
    redirect,
} from "bcp/server";

export async function loader({
    params,
    searchParams,
}: {
    params: {
        id: string;
    };
    searchParams: URLSearchParams;
}) {
    const session =
        await getSession();

    if (!session) {
        return redirect(
            "/login",
            303
        );
    }

    return {
        id:
            params.id,
        filter:
            searchParams.get(
                "filter"
            ),
        user:
            session,
    };
}

The matching page reads the loader result with useLoaderData<T>():

TypeScript
"use client";

import {
    useLoaderData,
} from "bcp";

interface UserLoaderData {
    id: string;
    filter: string | null;
    user: {
        sub: string;
    };
}

export default function UserPage() {
    const data =
        useLoaderData<
            UserLoaderData
        >();

    return (
        <main>
            <h1>
                User {data.id}
            </h1>
        </main>
    );
}

Loader context

A loader receives:

  • params — the matched file-route parameters, including dynamic segments.
  • searchParams — a fresh URLSearchParams instance for the target request URL.

Loaders run inside the normal BCP request context. Server-only helpers from bcp/server, including headers(), cookies(), requestUrl(), requestMethod(), requestId(), clientIp(), bearerToken(), getSession() and redirect(), are available while the loader is running.

For both initial document requests and SPA navigations, the loader request context is created for the target page URL rather than the internal /_bcp/navigation transport URL. For example, navigating to /users/42?filter=active makes requestUrl() observe /users/42?filter=active and gives the loader the matching query string.

This makes the recommended authenticated page flow:

text
Initial document request
    ↓
loader.ts
    ↓
Session / database / authorization
    ↓
serializable loader data
    ↓
SSR page
    ↓
useLoaderData() during hydration

Later <Link> / router navigation
    ↓
/_bcp/navigation
    ↓
loader.ts runs for the target URL
    ↓
fresh session / cookie / database state
    ↓
navigation payload with loader data
    ↓
client renders the target route

Database modules should remain server-only and can be imported directly by loader.ts.

Return values

A loader can return either serializable data or a Web Response.

Data

Loader data must contain only JSON-compatible values:

  • null
  • strings
  • booleans
  • finite numbers
  • arrays of supported values
  • plain objects containing supported values

Values such as Date, Map, Set, functions, symbols, BigInt, class instances, NaN, Infinity and circular object graphs are rejected. Convert database-specific values to JSON-safe values before returning them.

TypeScript
export async function loader() {
    const user =
        await findUser();

    return {
        id:
            user.id,
        createdAt:
            user.createdAt.toISOString(),
    };
}

The result is rendered through a server-side loader-data provider and serialized into BCP framework data. During SPA navigation the same data is carried in the navigation payload and written into the framework data before the target client bundle renders. loader.ts itself is never imported by the browser entry.

Redirects and other responses

A loader can return a standard Response, including the BCP redirect() helper:

TypeScript
import {
    cookies,
    redirect,
} from "bcp/server";

export async function loader() {
    const cookieStore =
        await cookies();

    cookieStore.set(
        "last_guard",
        "private-page",
        {
            httpOnly: true,
            sameSite: "lax",
            path: "/",
        }
    );

    return redirect(
        "/login",
        303
    );
}

Response cookies created during the loader are merged into the returned response, including redirects.

For an SPA navigation, BCP converts an HTTP redirect into a navigation redirect payload while preserving all Set-Cookie values. Same-origin redirects continue through the BCP router without a full page reload. Cross-origin redirects are handed to normal browser navigation. The client router limits a single navigation chain to 10 redirects to prevent redirect loops.

A loader may also return another Web Response. If that response is not a navigation payload or supported redirect response, the client router falls back to a normal document request so the original HTTP semantics are preserved.

Client navigation behavior in 0.1.9

Routes with and without loaders both support BCP SPA navigation.

When <Link>, navigate(), router.push(), router.replace(), browser history navigation, or router.refresh() targets a loader-backed page, BCP performs the following work:

  1. Starts navigation state immediately and aborts the previous in-flight navigation request.
  2. Requests /_bcp/navigation with the target pathname and query string.
  3. Runs the target route loader on the server inside a fresh request context.
  4. Returns route metadata, client asset information and the fresh loader data in one navigation payload.
  5. Writes the payload into __BCP_DATA__ before importing/rendering the target client bundle.
  6. Reuses the existing React root so persistent layouts can keep client state where React reconciliation allows it.

AbortController cancels superseded network requests. BCP also assigns each navigation a monotonically increasing sequence so a stale response that completes after a newer navigation is not allowed to update router state. The route bundle import receives the navigation identifier as its cache-busting query parameter as well.

loading.tsx remains part of the navigation payload. useNavigation() is set to navigating before the server request starts; once the payload arrives, the matching loading markup can be shown while target assets/runtime work completes.

Middleware and security

SPA loader navigation still travels through the normal BCP middleware and security gateway chain before reaching the loader navigation runtime. Applications should still enforce page authorization inside the loader itself because the loader owns the target page data and can make the authorization decision using getSession(), cookies, headers or database state.

The internal /_bcp/navigation endpoint is transport infrastructure and is not a substitute for loader-level authorization checks.

Response cache behavior

Loader-backed pages are excluded from the production response-cache manifest by default. A loader can depend on cookies, sessions, authorization headers, request identity or user-specific database state, so automatically placing the resulting HTML in a public route cache would be unsafe.

Caching inside a loader can still be implemented explicitly with application-aware data caching where the cache key and invalidation model are known.

Navigation payloads are sent with Cache-Control: no-store and X-Content-Type-Options: nosniff.

Error and not-found behavior

Errors thrown by a loader enter the same page error handling path as SSR errors. For navigation requests, server errors or unsupported response shapes cause the router to fall back to a normal document request so the page-level error/not-found rendering path remains authoritative.

A loader can also call existing server/runtime primitives that throw the framework not-found signal; the matching not-found.tsx boundary remains responsible for rendering the 404 page.

Development reloads

loader.ts and loader.tsx are watched as server files during development. Loader modules use a server-side cache-busting version during document rendering and navigation, so edits are reflected without putting loader code in the browser dependency graph.

Only one loader file may exist next to a page. Having both loader.ts and loader.tsx is treated as a framework configuration error.