skip to content

In AnalogJS, how does a catalogue page load its product data in a .server.ts load function, and how does the page component read it?

level: middleimportance: should knowfreq 27%

answer

  1. a sibling file that never ships
  2. typed with PageServerLoad
  3. an Angular resolver underneath
  4. an endpoint below /api/_analog/pages
  5. injectLoad hands back an Observable

basics

~20 s

In AnalogJS, an async load exported from catalogue.server.ts runs only on the server and returns JSON-serialisable data. Analog resolves it before the page component is created, and the component reads it with injectLoad<typeof load>(), usually wrapped in toSignal.

solid answer

~40 s

Beside `catalogue.page.ts` you add `catalogue.server.ts` with `export const load = async ({ params, event, fetch }: PageServerLoad) => ({ ... })`. Analog turns that file into a Nitro handler at `/api/_analog/pages/catalogue` and adds a `load` resolver to the page's generated route that fetches that URL through `HttpClient`, so the result is in the route's data before the component exists. In the component, `injectLoad<typeof load>()` returns an Observable of that result; `toSignal(injectLoad<typeof load>(), { requireSync: true })` makes it a signal with no undefined state. Importing `load` just for `typeof` is safe because the client build empties page `.server.ts` modules. By default the resolver runs again when params or query params change, and whatever `load` returns can be fetched by anyone who calls that endpoint.

code

ts · 17 lines
ts
// src/app/pages/catalogue.server.ts  (server only)
import type { PageServerLoad } from '@analogjs/router';
import { getQuery } from 'h3';

type Product = { sku: string; name: string; priceCents: number };

export const load = async ({ event }: PageServerLoad) => {
  const category = String(getQuery(event)['category'] ?? 'all');
  const res = await fetch(
    `${process.env['CATALOGUE_API_URL']}/products?category=${encodeURIComponent(category)}`,
    { headers: { authorization: `Bearer ${process.env['CATALOGUE_API_TOKEN']}` } },
  );
  const products = (await res.json()) as Product[];

  // The token stays here; this object is served to any caller of the endpoint.
  return { category, products };
};

go deeper

for a junior

Recall the pairing: catalogue.page.ts for the component, catalogue.server.ts for an async load, and injectLoad in the component to read the result.

for a middle

Explain the mechanics: a Nitro endpoint under /api/_analog/pages, a generated resolver that fetches it, injectLoad as an Observable over route data, and why toSignal with requireSync is safe.

for a senior

Show production judgment: load's output is publicly fetchable, re-runs on param or query changes, is serialised into transfer state on the first render, and needs staticData on a static-only build.

for a principal

Decide what belongs in a per-page load versus a shared API route or a server function, weighing coupling to one page against reuse and a public contract.

## The two files AnalogJS pairs a page with an optional **server module** of the same name: - `src/app/pages/catalogue.page.ts`: the Angular component for `/catalogue` (default export). - `src/app/pages/catalogue.server.ts`: server-only code for that page. It may export an async **`load`** (data for the page) and an `action` (a form post handler). `load` receives one argument typed **`PageServerLoad`** from `@analogjs/router`, with these fields: | Field | What it is | |---|---| | `params` | the route parameters of the request, for example `slug` for `blog/[slug].server.ts` | | `req`, `res` | the underlying Node request and response objects | | `event` | the full h3 event, for h3 helpers such as `getQuery(event)` or `getCookie(event, name)` | | `fetch` | Nitro's `$fetch`, which can call your own API routes without a network hop | Whatever `load` returns is sent to the page as **JSON**, so return plain objects: a `Date` arrives as a string and a class instance loses its methods. ## What Analog does with them 1. At build time, every `*.server.ts` under `src/app/pages` becomes a **Nitro event handler** served at `/api/_analog/pages/<route>`; `catalogue.server.ts` becomes `/api/_analog/pages/catalogue`, and `products/[id].server.ts` becomes `/api/_analog/pages/products/:id`. 2. For each page, Analog adds a **`load` entry to the generated route's `resolve`**. That resolver builds the endpoint URL from the current route (parameters and query string included) and requests it with Angular's `HttpClient`. 3. Because it is a resolver, navigation waits for it, so the data is already in the route's `data['load']` when the component is created. 4. During the first, server-rendered request the resolver runs on the server, and the response is stored in Angular's `TransferState`, so the hydrating browser reuses it instead of requesting it again. Analog's docs stress that this reuse happens on that first request only. 5. On later **client-side navigations** the browser's resolver makes a real GET to the endpoint, and `load` runs on the server again. 6. In the **client build** a page's `.server.ts` is replaced with an empty module, so the page can `import { load }` for its type without shipping server code or secrets. ## Reading the data in the component `injectLoad<typeof load>()` must be called in an **injection context** (a field initializer or constructor), or given an `injector` option. It returns an **Observable** of `Awaited<ReturnType<typeof load>>`, read from `ActivatedRoute.data`. The docs pair it with `toSignal(..., { requireSync: true })`: the route data already holds the value when the component is created, so the signal never starts as `undefined`. Two alternatives exist: - **Component input binding**: with `provideFileRouter(withComponentInputBinding())`, a component input named `load` receives the result; `LoadResult<typeof load>` types it. - **Another resolver**: inside a `resolve` function in `routeMeta`, `getLoadResolver(route)` returns the same server data, for example to derive a page title from it. ## When load runs again Analog sets the generated route's **`runGuardsAndResolvers`** to `'paramsOrQueryParamsChange'` unless the page's `routeMeta` sets another value. So changing `/catalogue?category=lamps` to `?category=chairs` re-runs the resolver, a new request reaches `load`, and `injectLoad`'s Observable emits the new result. A page without a `.server.ts` still has the resolver, which then simply returns `{}`. ## What load is, and what it is not | | `load` in `.server.ts` | `resolve` entry in `routeMeta` | API route in `src/server/routes/api` | |---|---|---|---| | Runs | on the server only | wherever the router runs (server and browser) | on the server only | | Called by | the page's generated resolver | the Angular router | anything that sends an HTTP request | | Reached over HTTP | yes, at `/api/_analog/pages/...` | no | yes, under `/api` | | Typical use | one page's data | derived or client-side data | data or actions shared by many callers | ## Pitfalls - **Treating the return value as private.** The endpoint is public: anyone can GET `/api/_analog/pages/catalogue`. Keep API tokens inside `load`, return only what the page renders, and check authorisation inside `load` itself. - **Expecting `load` to run in the browser.** It never does; the browser only calls its endpoint. - **Reading `injectLoad` as a signal.** It is an Observable; convert it. - **Calling `injectLoad` outside an injection context.** It uses `inject()` internally, so call it in a field initializer or constructor, or pass `{ injector }`. - **Slow work in `load`.** The resolver blocks navigation until `load` answers, so a slow upstream call delays the whole page, on the server and on every client navigation. - **Prerendered pages on a static host.** Client navigations still call the endpoint, so a static-only build needs the data prerendered too (`staticData: true` on that prerender route).

  • In AnalogJS, why must a .server.ts load never return an API token or an internal cost price?
    Because its return value is served from a public endpoint, `/api/_analog/pages/<route>`, that anyone can GET, and during SSR the response is also serialised into the page's transfer state for hydration. Keep secrets inside `load`, return only what the page renders, and enforce authorisation inside `load`, since callers can reach the endpoint without going through the page.
  • In AnalogJS, does load run again when only ?category= changes on the same page?
    Yes, by default. Analog sets the generated route's `runGuardsAndResolvers` to `'paramsOrQueryParamsChange'` unless `routeMeta` overrides it, so the resolver requests the endpoint again with the new query string, `load` runs on the server, and `injectLoad`'s Observable emits the new result.
  • In AnalogJS, how can a routeMeta resolver reuse the server data that load produced?
    Call `getLoadResolver(route)` from `@analogjs/router` inside the resolver. It returns the result of the page's server `load` for that route, so you can derive a title or meta tags from it without a second endpoint.

A page's .server.ts load is like a restaurant kitchen behind a serving hatch: diners never see the recipes or the supplier accounts, only the plates passed through, and anyone who walks up to the hatch can ask for a plate.

saying these in an interview costs you the question

  • Analog runs the load function in the browser during client-side navigation.
  • Importing load into the page ships its server code and secrets to the browser.
  • injectLoad returns a signal, so no toSignal call is needed.
  • Whatever load returns stays on the server, so tokens are safe in it.
  • A .server.ts load is an Angular ResolveFn you add to the routes array yourself.