fetchMetadata
Fetch route metadata from the server instead of bundling it in the client.
Route metadata is what the client needs to call a route: its param and return types, its options, and its validation and JSON round trip functions. By default, the mion build plugin bundles it into your client code.
Use this middleware when:
- You don't want the routes metadata in the client bundle, for example to keep the bundle small for a large API.
- Your client is built in a separate project from your server, or without the mion build plugin.
When you deploy a server that changes a route, the client fetches it again, with no client rebuild.
Getting Route Metadata
A fetched client gets the server's own compiled functions, so they always match it.
| Mode | How the client gets it | What you set up |
|---|---|---|
| Bundled (default) | The build ships it with the client, for every route the client calls. | Nothing. |
| Fetched | The mionFetchMetadata middleware sends it on a route's first call, and the client saves it. | The middleware and the client setup. |
Adding the Middleware
Import mionFetchMetadata from @mionjs/router/middlewares and add it to your routes. Only an API whose clients fetch metadata needs it.
import {createMionRouter, Routes} from '@mionjs/router';
import {mionFetchMetadata} from '@mionjs/router/middlewares';
const mion = createMionRouter();
const routes = {
// at the root, before any route or group, or the router refuses it
mionFetchMetadata,
sayHello: mion.route((ctx, name: string): string => `Hello ${name}`),
} satisfies Routes;
const myApi = mion.initRoutes(routes);
export type MyApi = typeof myApi;
What the Middleware Answers
Given a list of route ids, it returns the metadata of each route and of every middleware that runs with it.
| It describes | It never describes |
|---|---|
| Every route | Your handler code |
| Middleware that takes params or headers, or returns data | Raw middleware, and middleware with no params and no return data |
| The compiled validation and JSON round trip functions of those | mion's internal routes |
Why Metadata Is Public
Any client can ask for any route's metadata, without logging in. A client with bundled metadata has the same metadata in its JavaScript. Treat it like a public API schema: it shows your types, never your data.
Listing Every Method
A client written without mion can POST this body to any route path to list every public route and middleware, and the ids of your batches, so it can call a batch by id. The route at that path does not run.
{"mionFetchMetadata": [[], "all"]}
Method List Limit
When the API has more routes and middleware than the getAllRemoteMethodsMaxNumber router option, you get no full list: the answer has only the ids you sent, no batch ids, and truncated: true.
import {createMionRouter} from '@mionjs/router';
// list up to 200 methods when a client asks for all of them (100 by default)
export const mion = createMionRouter({getAllRemoteMethodsMaxNumber: 200});
Disabling Routes Metadata Bundling
Set client: {routes: 'fetch'} in your client build, then call useFetchMetadata once, next to initClient, with the client's middlewares.mionFetchMetadata. The server must also add the middleware.
import {defineConfig} from 'vite';
import {mionVitePlugin} from '@mionjs/devtools/vite';
export default defineConfig({
plugins: [
mionVitePlugin({
// fetch each route's metadata from the server on first use
client: {routes: 'fetch'},
}),
],
});
import {initClient} from '@mionjs/client';
import {useFetchMetadata} from '@mionjs/client/middlewares';
import type {MyApi} from './metadata-fetch.routes.ts';
const {routes, middlewares} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
});
useFetchMetadata(middlewares.mionFetchMetadata);
// the first call also fetches the metadata of sayHello
const [greeting] = await routes.sayHello('Ana').call();
console.log(greeting);
| Tool | Disable routes metadata bundling |
|---|---|
| Vite plugin | client: {routes: 'fetch'} |
Next.js withMion | client: {routes: 'fetch'} |
| mion CLI | --client-routes fetch |
| tsconfig plugin entry | clientRoutes: 'fetch' |
metadata-load-error in its @thrownErrors, and the next call tries again.Fetching Only What the Build Misses
If you keep bundling on and also call useFetchMetadata, the client fetches metadata only for calls the build could not bundle: calls whose route a generic helper hides. Fetched metadata never replaces a bundled route. To keep every call bundled, see Generic Call Helpers.
| Client | A call whose route the build cannot see |
|---|---|
Without useFetchMetadata | Stops the build with rpc-client-route-id-widened. If it runs anyway, it gets route-metadata-not-found in its @thrownErrors, naming the fix. |
With useFetchMetadata | Fetches its metadata instead, so a strict Content Security Policy blocks it. The build reports rpc-client-route-id-widened-fetched, an Info message shown only with levels: 'all'. |
The First Call
A route's first call goes out right away and asks for the metadata in the same request. The server validates the params and answers with the result and the metadata. The client saves the metadata, so later calls also validate in the browser before sending.
| Case | What the client does |
|---|---|
Params that JSON.stringify cannot write | Fetches the metadata first, then sends the call. |
A param that needs its type to be encoded, like a string | Date union | Sends the call once more with the right encoding. The route did not run. |
typeErrors() on a route it has not seen | Fetches the metadata alone. The route does not run. |
| A saved copy that no longer matches the server | Drops the saved copy and sends the call once more. |
Saving Fetched Metadata
The client saves what it fetched, per server, so a later visit does not ask again. Pick where with the storageEngine client option. Where the store cannot open, like on the server or in a browser that blocks IndexedDB, the client keeps it in memory.
import {initClient, requestPersistentStorage} from '@mionjs/client';
import {useFetchMetadata} from '@mionjs/client/middlewares';
import type {MyApi} from './metadata-fetch.routes.ts';
const {middlewares} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
storageEngine: 'indexeddb',
});
useFetchMetadata(middlewares.mionFetchMetadata);
// ask the browser to keep the cache when it clears storage, best after a user action
document
.querySelector('#settings')
?.addEventListener('click', () => requestPersistentStorage());
storageEngine | Where it saves |
|---|---|
'indexeddb' (default) | The browser's IndexedDB, kept between visits. |
'memory' | Memory, for this page load or process only. |
| A function | Your own store. |
metadata-cache-error in its @thrownErrors, once.requestPersistentStorage() yourself, as in the example.Using Your Own Store
Set storageEngine to a function that opens your own store. The client calls it once. Your store implements MetadataStore from @mionjs/client: readAll, write, remove and clear, each for one server's baseURL.
import {initClient, type MetadataStore} from '@mionjs/client';
import type {MyApi} from './metadata-fetch.routes.ts';
// your app's own storage, like a native app's database
declare function openAppStore(): Promise<MetadataStore | undefined>;
export const {routes, middlewares} = initClient<MyApi>({
baseURL: 'http://localhost:3000',
// returning undefined keeps the cache in memory
storageEngine: openAppStore,
});
write must save the whole batch of records or reject. readAll returns the records of that exact baseURL, never a prefix match.API Version Check
mionFetchMetadata sends the server's API version in the x-build-version header of every response, and the client compares it with the version it was built with. A mismatch does not stop the call: reload the app or rebuild the client to fix it. Use mionSyncRoutes to stop such calls, or run mion api-check to catch a mismatch before you deploy.
| Client | On a mismatch |
|---|---|
Without useFetchMetadata | The call that sees it gets an api-version-mismatch error in its @thrownErrors, once per server. |
With useFetchMetadata | Each route is checked once, on its first call after the mismatch. A changed route whose metadata was fetched is fetched again. A changed bundled route gets api-version-mismatch, naming the route. |
mionFetchMetadata, or with apiVersionCheck: false in createMionRouter, the server sends no version and the client checks nothing.Build Checks
The build fails on a setup that would fail at runtime. Look up any name in All Diagnostics.
| Name | When |
|---|---|
| rpc-client-api-unreadable | The client is not typed with PublicApi<typeof routes>. |
| rpc-client-route-not-declared | A call names a route the API type does not declare. |
| rpc-client-route-id-widened | A generic helper hides a call's route, and the client does not fetch metadata. |
| rpc-client-version-mismatch | A client and an API in one program are typed with different APIs. |
| rpc-client-middleware-not-set-up, rpc-client-optional-middleware-not-set-up | A called route runs a middleware the client never sets up. |
| rpc-client-no-metadata-route | The client fetches metadata, but the API does not add mionFetchMetadata. |
| rpc-client-fetch-not-set-up | The client builds with client: {routes: 'fetch'}, but never calls useFetchMetadata. |