Docs/Core Concepts/Data Loading
GitHub

Data Loading

Data loaders run on the server and provide data to your pages. Create a page.data.ts file next to page.ts.

# Basic loader

// src/app/page.data.ts
import type { PageDataLoad } from "@deijose/nix-js-kit";

export const load: PageDataLoad = async () => {
  return { title: "Hello World", posts: ["a", "b", "c"] };
};
// src/app/page.ts
import { html } from "@deijose/nix-js";
import type { PageProps } from "@deijose/nix-js-kit";
import { load } from "./page.data.ts";

export default function HomePage({ data }: PageProps<typeof load>) {
  return html`
    <h1>${data.title}</h1>
    <ul>${data.posts.map((p) => html`<li>${p}</li>`)}</ul>
  `;
}
💡 Tip

PageProps<typeof load> automatically infers the data type from your loader. No manual interface needed.

# Load context

Loaders receive a context with params, searchParams, and request:

export const load: PageDataLoad = async ({ params, searchParams, request }) => {
  const slug = params.slug;                    // dynamic route param
  const page = searchParams.get("page") ?? "1"; // query string
  const cookie = request?.headers.get("Cookie"); // SSR cookies

  return { slug, page, user: await getUser(cookie) };
};

# LoadContext

interface LoadContext {
  params: RouteParams;           // { slug: "hello" } or { slug: ["a","b"] }
  searchParams: URLSearchParams; // ?page=1&tab=info
  request?: Request;             // available in SSR/streaming modes
}
ℹ Note

The request object is available in SSR (nix-js-kit start), streaming, and the Vite dev server. In pure SSG (nix-js-kit build), it's undefined because there's no HTTP request.

# RequestContext (v2)

Since v2.0.0, the unified Web handler exposes a richer RequestContext that carries per-request state across middleware, loaders, and actions. It's available from @deijose/nix-js-kit/runtime:

import type { RequestContext } from "@deijose/nix-js-kit/runtime";

// Fields:
//   params      — RouteParams (dynamic segment values)
//   locals      — Record<string, unknown> (per-request mutable state, isolated per request)
//   cookies     — CookieJar (read request cookies: get, getAll, has)
//   signal      — AbortSignal (aborts when the client disconnects)
//   requestId   — string (auto-generated UUID for logging/tracing)
//   platform    — { runtime: "node" | "bun" | "edge", host: string }
//   route       — PageRoute (the matched route)
//   response    — ResponseState (mutable headers + Set-Cookie accumulation)
//   applyToResponse() — merges accumulated headers/cookies/status into a Response

Middleware and the unified handler use RequestContext internally. Loaders receive a LoadContext view (backwards compatible); if you need the full context (e.g. signal for abortable fetches), access it via the runtime entry:

import { streamBoundary } from "@deijose/nix-js-kit";

export const load = async ({ params, request }) => {
  // Use request.signal for abortable data fetching
  const res = await fetch("https://api.example.com/posts", {
    signal: request?.signal ?? undefined,
  });
  return { posts: await res.json() };
};

# Dynamic routes with generateStaticParams

// src/app/blog/[slug]/page.data.ts
import type { PageDataLoad } from "@deijose/nix-js-kit";

export const load: PageDataLoad = async ({ params }) => {
  const post = await getPost(params.slug as string);
  return { post };
};
// src/app/blog/[slug]/page.ts
import type { GenerateStaticParams } from "@deijose/nix-js-kit";

export const generateStaticParams: GenerateStaticParams = async () => {
  const posts = await getAllPosts();
  return posts.map((p) => ({ slug: p.slug }));
};

# ISR with revalidate

Export revalidate from page.data.ts to enable incremental static regeneration:

// src/app/blog/[slug]/page.data.ts
export const load = async ({ params }) => {
  return { post: await getPost(params.slug) };
};

export const revalidate = 300; // revalidate every 5 minutes

The first request serves the cached HTML. After revalidate seconds, the next request regenerates the page and updates the cache.

# Layout data loaders

layout.data.ts provides data to layouts:

// src/app/layout.data.ts
export const load = async () => {
  return { nav: await getNav() };
};

The layout receives it via LayoutProps.data:

export default function Layout({ children, data }: LayoutProps<typeof load>) {
  return html`<nav>${data.nav.map(...)}</nav><main>${children}</main>`;
}

# Request context for auth

// src/app/dashboard/page.data.ts
export const load = async ({ request }) => {
  if (!request) return { user: null };

  const session = parseSession(request.headers.get("Cookie"));
  if (!session) {
    throw new Response("Unauthorized", { status: 401 });
  }

  return { user: await getUser(session.userId) };
};
⚠ Warning

Cookies are forwarded to loaders in start, preview, dev, and the Vite plugin. Make sure your CSRF and auth middleware run before loaders if you need to block requests early.

✎ Edit this page on GitHub