G · 08 guides

Guides / Data Fetching

Data Fetching

aihu provides several primitives for fetching data, ranging from reactive resource signals declared in @state, to a standalone resource utility for use outside SFCs, to server-side loaders for SSR routes.

resource() in @state

resource() is one of the @state wrapper intrinsics (alongside state(), prop(), derived(), action()) — it binds an async fetcher to a reactive signal. Bare (no metadata) or wrapped (config object first):

governedaihuaihu423 B
@state {
  let userId = prop<number>({ default: 1 })

  // bare — just the fetcher thunk
  const user = resource(() => fetchUser(userId))

  // wrapped — add describe/expose to surface to agents
  const recentPosts = resource(
    { describe: 'Posts by the current user', expose: 'read' },
    () => fetchPosts(userId),
  )
}

@template {
  <suspense fallback="Spinner">
    <div>{user.data?.name}</div>
  </suspense>
}

The resource variable is a 4-field loader object:

  • resource.loadingtrue while the fetch is in-flight.
  • resource.data — the resolved value (or null if loading/error).
  • resource.error — the error (or null if loading/success).
  • resource.refetch() — re-runs the fetcher on demand.

When any signal read inside the fetcher changes (e.g. userId), the resource re-fetches automatically.

The loader-state pattern

All resource() data in aihu follows the same 4-field shape:

State loading data error
Loading true null null
Success false data null
Error false null Error

<suspense fallback="..."> wraps a resource consumer to declare a loading fallback in the template:

governedaihuaihu72 B
<suspense fallback="Spinner">
  <div>{user.data?.name}</div>
</suspense>

Current limitation. <suspense> parses and validates today, but its runtime boundary is presently a pass-through stub — it always renders the wrapped content rather than gating on the resource's loading state. Branch on resource.loading directly (if={user.loading} / else) if you need a working loading state today; treat <suspense> as declaring intent ahead of the boundary becoming functional.

createResource from @aihu-plugin/data

Use createResource directly in TypeScript outside of SFCs. Unlike the @state resource() intrinsic, this is a lower-level, standalone API with its own return shape:

governedtypescripttypescript543 B
import { createResource } from '@aihu-plugin/data'
import { signal } from '@aihu/signals'

const userId = signal<string | null>('1')
const user = createResource(
  userId,                                                // key — a Signal; changes trigger refetch
  (id) => fetch(`/api/users/${id}`).then(r => r.json()), // fetcher — receives the resolved key
)

// user.state is a Signal<DataState<T>>, a status-discriminated union:
//   { status: 'idle' | 'loading' | 'ready' | 'error' | 'streaming', ... }
user.refetch()
user.invalidate()

The key argument is a signal (not a thunk) holding the current key value; the resource automatically re-fetches when it changes. user.state() reads the current DataState; check .status to discriminate loading/ready/error/streaming.

Resource store and SSR dehydration

For SSR, use a resource store to cache and dehydrate resources: createResourceStore, createResourceSerializer, and the ResourceStoreToken injection token are all exported from @aihu-plugin/data alongside createResource.

Register the data plugin in aihu.config.ts:

governedtypescripttypescript147 B
import { defineAihuConfig } from '@aihu/server'
import { data } from '@aihu-plugin/data'

export default defineAihuConfig({
  plugins: [data()],
})

aihu.config.ts/defineAihuConfig is the legacy fallback for general app/build config (see the Deployment guide — the primary app config surface is now the inline viteAihuPlugin({...}) in vite.config.ts), but it remains the live, current registration path specifically for @aihu/plugin-shaped compiler plugins like this one. viteAihuPlugin({...})'s own plugins field is a different thing — it takes Vite plugins, not @aihu/plugin plugins — so data() belongs in defineAihuConfig, not there. See the Authoring Plugins guide for the full registration model.

Server loaders

Server loaders run on the server and provide data to SSR-rendered pages. Define a loader with defineLoader from @aihu/server:

governedtypescripttypescript160 B
import { defineLoader } from '@aihu/server'

export const loader = defineLoader(async (ctx) => {
  const users = await db.users.findMany()
  return { users }
})

The loader receives a LoaderContext with the request, params, and URL. What happens to the return value next depends on whether the route is governed.

Server loaders → SFC handoff

There are two distinct paths, and only one of them delivers a route.data prop.

Governed routes — route.data prop

A route becomes governed by declaring a data: field in its @route block (data: { type: 'TypeName', preview: [...] }). This opts the route into aihu's entitlement-gated data system: the loader's result is evaluated against the request's principal, and the component receives the resolved value as the data field on its route prop — a fully entitled payload for an authorized caller, or a withheld/preview-only shape otherwise.

src/pages/posts/[slug].loader.ts:

governedtypescripttypescript158 B
import { defineLoader } from '@aihu/server'

export const loader = defineLoader(async (ctx) => {
  return await db.posts.findOne({ slug: ctx.params.slug })
})

src/pages/posts/[slug].aihu:

governedaihuaihu294 B
@route {
  path: "/posts/[slug]"
  ssr: true
  data: { type: "Post", preview: ["title"] }
}

@state {
  let route = prop<{ params: { slug: string }; data: { title: string; body: string } }>()
}

@template {
  <article>
    <h1>{route.data.title}</h1>
    <p>{route.data.body}</p>
  </article>
}

The full entitlement-gating semantics (what a non-authorized caller's route.data looks like, how preview fields are chosen) are beyond this guide's scope — this is the pattern to reach for when a page's data needs to vary by who's asking (paywalled/tiered content, per-user views).

Plain routes — no props, inline JSON only

A route with no data: declaration still runs its defineLoader, but the SFC itself receives no props from it. The result is embedded in the response as an inline script tag:

governedhtmlhtml77 B
<script type="application/json" id="__aihu_loader__">{"users":[...]}</script>

There is currently no built-in client-side reader for this tag — if the component needs the value, read and JSON.parse it yourself (e.g. in an onMount()). Use a plain loader when you only need the data to influence the HTML aihu itself renders (e.g. via a top-level render function), not when the SFC needs it as a prop.

On-demand client→server calls. aihu previously shipped a $server macro and a createServerCall client stub for calling server functions from the client over RPC. Both were retired — the feature never fully wired up (see the Macro Vocabulary spec §2.12) — with no drop-in replacement. For data that depends on post-load UI state, fetch from a server route (defineApiRoute / defineStreamRoute in @aihu/server) with a plain fetch, or drive it through a governed loader.

Server response helpers

From @aihu/server:

Helper Description
json(data, status?) Return a JSON response
notFound() Return a 404 response (no message argument)
badRequest(message?) Return a 400 response
serverError(err?) Return a 500 response (message suppressed unless DEV)
methodNotAllowed(allowed) Return a 405 response for the given array of allowed HttpMethods
defineApiRoute(method, pattern, handler) Declare a method+pattern-matched API route

Streaming routes

For streaming HTTP responses (e.g. server-sent events), use defineStreamRoute from @aihu/server. The handler returns a ReadableStream<string> — there's no ctx/writer callback form:

governedtypescripttypescript302 B
import { defineStreamRoute } from '@aihu/server'

export const events = defineStreamRoute(async (req) => {
  return new ReadableStream({
    start(controller) {
      controller.enqueue('event: connected\ndata: {}\n\n')
      // ... enqueue further frames, then:
      controller.close()
    },
  })
})

defineStreamRoute sets streaming-appropriate response headers automatically.