Fetcher

Call your Hono API with end-to-end type safety.

Use this by default

In a TanStack Start app, use fetcher through a plugin API client. The same request works during SSR and browser navigation.

During SSR, VitNode forwards the visitor’s request to the API. In the browser, it calls /api/* directly. You do not need to write createIsomorphicFn() or choose a transport.

Create your API client once

Define it in your plugin

Keep this in one plugin file. Features import notesApi; they never set up a module reference themselves.

plugins/site-notes/src/api/client.ts
import type { notesModule } from "../api/notes.module"

import { createApiClient } from "@vitnode/core/tanstack/fetcher"

export const notesApi = createApiClient<typeof notesModule>("@acme/site-notes")

Fetch data

plugins/site-notes/src/features/notes/notes-query.ts
import { queryOptions } from "@tanstack/react-query"

import { notesApi } from "../../api/client"

export const notesQueryKey = ["@acme/site-notes", "notes"] as const

export const notesQuery = () =>
  queryOptions({
    queryKey: notesQueryKey,
    queryFn: async ({ signal }) => {
      const response = await notesApi.fetch({
        method: "get",
        module: "notes",
        options: { signal },
        path: "/",
      })

      if (!response.ok) {
        throw new Error(`The notes API answered ${response.status}.`)
      }

      return await response.json()
    },
  })

Use it on a page or in a mutation

Warm the query in the route loader. The component reads that same cache entry with useQuery(notesQuery()).

plugins/site-notes/src/routes/notes.tsx
import { definePluginRoute } from "@vitnode/core/routing"

import { notesQuery } from "../features/notes/notes-query"

export const route = definePluginRoute({
  load: async ({ context }) =>
    await context.queryClient.ensureQueryData(notesQuery()),
})

Use the same API client, then invalidate the data that changed.

plugins/site-notes/src/features/notes/create-note.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query"

import { notesApi } from "../../api/client"
import { notesQueryKey } from "./notes-query"

export const useCreateNote = () => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async (title: string) => {
      const response = await notesApi.fetch({
        args: { body: { title } },
        method: "post",
        module: "notes",
        path: "/",
      })

      if (!response.ok) throw new Error("Could not create the note.")

      return await response.json()
    },
    onSuccess: async () =>
      await queryClient.invalidateQueries({ queryKey: notesQueryKey }),
  })
}

Server-only work

Use @vitnode/core/tanstack/fetcher/server only for a server function, cookie relay, cron/job, secret, or a custom API origin.

Keep it server-only

Put code that imports this fetcher in a *.server.ts file, or call it only from a server function.

What the types do

  • method, module, and path are always required.
  • args is required when the route declares a body, params, or query.
  • TypeScript infers the valid route, arguments, response status, and JSON body.

Generated Content Engine modules have no static module type, so use rawFetcher for them instead.