mion provides a system for defining pure functions that can be safely transmitted over the network between server and client. All pure functions must follow strict purity rules so they can be serialized, transmitted, and executed safely on either side.
There are three main APIs for working with pure functions, each with a different direction of flow:
| API | Defined in | Direction | Purpose |
|---|---|---|---|
registerPureFnFactory() | Server code | Server → Client | Register a utility function on the server that can be shared with clients over the network |
pureServerFn() | Client code | Client → Server | Define a function in client code that gets extracted and compiled into the server at build time |
mapFrom() | Client code | Client → Server | Map the output of one route to the input of another within a routesFlow — compiled into server at build time |
All three follow the same purity rules and are processed by the mion Vite plugin at build time.
registerPureFnFactory() is used in server code to register pure utility functions that can be shared with clients over the network. These are typically used by mion's JIT compilation system — for example, validation helpers, format functions, or computation utilities that both server and client need.
The factory function receives jitUtils as its single parameter, giving it access to other registered pure functions. The factory is called once at initialization time to create the actual function:
// Register a validation utility on the server
registerPureFnFactory('myNamespace', 'limitItems', function (jitUtils) {
const MAX_ITEMS = 100;
return function inner(items: any[]) {
return items.slice(0, MAX_ITEMS);
};
});
// Register a factory that depends on another pure function
registerPureFnFactory('myNamespace', 'validateAndLimit', function (jitUtils) {
const limitFn = jitUtils.getPureFn('myNamespace', 'limitItems');
return function inner(items: any[]) {
const filtered = items.filter((item) => item != null);
return limitFn(filtered);
};
});
Because these functions are pure (no closures, no side effects, no forbidden globals), they can be safely serialized and sent to clients when needed — for example, when a client requests JIT functions for validation or serialization.
pureServerFn() is used in client code to define functions that will be extracted at build time and compiled into the server. This allows clients to define logic that runs safely on the server without the server ever executing arbitrary client code.
At runtime on the client, pureServerFn() returns a lightweight PureServerFnRef — a reference containing the function's hash identifier that the client sends to the server.
// Simple pure function — defined in client, runs on server
const double = pureServerFn((x: number) => x * 2);
// Named pure function
const compute = pureServerFn(function addOne(x: number) {
const result = x + 1;
return result;
});
// Factory variant — called once at server init to create the actual function
const validate = pureServerFn({
pureFn: function factory() {
const regex = new RegExp('^[a-z]+$');
return function inner(s: string) {
return regex.test(s);
};
},
isFactory: true,
});
pureServerFn() callsbodyHashbodyHash is injected as an additional argument via a TypeScript transformervirtual:mion-server-pure-fns virtual moduleAt runtime, when the client sends a request referencing a pure function by its hash, the server looks it up in the virtual module and executes the server-side copy — never running arbitrary client code.
mapFrom() is used in client code within a routesFlow to map the output of one route to the input of another. Like pureServerFn, the mapper function is defined in the client but extracted and compiled into the server at build time. This enables GraphQL-style data composition without extra round trips.
const {routes} = initClient<MyApi>({baseURL: 'http://localhost:3000'});
async function getOrderWithUser() {
const order = routes.orders.getById('ORDER-123');
// mapFrom maps order.userId → users.getById input (runs server-side)
const mapping = mapFrom(order, (o) => o!.userId);
// fake() returns a typed placeholder that satisfies the TypeScript compiler
const user = routes.users.getById(mapping.type());
const [[orderData, userData]] = await routesFlow([order, user]);
console.log(`Order by ${userData?.name}`);
}
The mapping.fake() call returns a typed placeholder that satisfies the TypeScript compiler, while at runtime the server uses the mapper function to extract the actual value from the order result.
Pure functions with params, local variables, and allowed globals:
// Pure function using only params and allowed globals
pureServerFn((x: number) => Math.floor(x * 2));
// Pure function with local variables
pureServerFn(function compute(x: number) {
const y = x * 2;
const z = y + 1;
return z;
});
// Pure function with nested callbacks and destructuring
pureServerFn((items: number[]) => items.map((x) => x + 1).filter((x) => x > 0));
pureServerFn(({a, b}: {a: number; b: number}) => a + b);
Using allowed globals (JSON, Math, RegExp, console, etc.):
// Pure functions can use allowed globals: JSON, Math, RegExp, console, etc.
pureServerFn(function validate(s: string) {
const encoded = encodeURIComponent(s);
const num = parseInt(s, 10);
const valid = isNaN(num) ? false : isFinite(num);
const regex = new RegExp('^[a-z]+$');
console.log(encoded, valid, regex);
return num;
});
// Pure function with try-catch
pureServerFn(function safeParse(s: string) {
try {
return JSON.parse(s);
} catch (e) {
return null;
}
});
Factory functions — same purity rules, local scope only:
// Factory functions follow the same purity rules — only local scope is allowed
pureServerFn({
pureFn: function factory() {
const regexp = new RegExp('^[a-z]+$');
return function inner(s: string) {
return regexp.test(s);
};
},
isFactory: true,
});
// registerPureFnFactory — local variables within the factory are allowed
registerPureFnFactory('ns', 'limitItems', function () {
const MAX_ITEMS = 100;
return function inner(items: any[]) {
return items.slice(0, MAX_ITEMS);
};
});
mapFrom — valid mappers:
// mapFrom() mapper follows the same purity rules as pureServerFn
const someSubRequest = {} as any; // placeholder for SubRequest
mapFrom(someSubRequest, (value: number) => value * 2);
mapFrom(someSubRequest, (user: {name: string; age: number}) => user.name.toUpperCase());
mapFrom(someSubRequest, function extractId(item: {id: number}) {
return item.id;
});
Impure operations (this, await, dynamic import):
// Using 'this' keyword
pureServerFn(function () {
return this.x; // ❌ Error: 'this' is not allowed in pure functions
});
// Using async/await
pureServerFn(async (x: number) => await x); // ❌ Error: async/await is not allowed
// Using dynamic import()
pureServerFn((x: string) => import(x)); // ❌ Error: Dynamic import() is not allowed
Forbidden identifiers (eval, fetch, setTimeout, process, window):
// Forbidden identifiers: eval, fetch, setTimeout, process, window, etc.
pureServerFn((x: string) => eval(x)); // ❌ Error: "eval" is not allowed
pureServerFn((url: string) => fetch(url)); // ❌ Error: "fetch" is not allowed
pureServerFn(() => {
setTimeout(() => {}, 100); // ❌ Error: "setTimeout" is not allowed
});
pureServerFn(() => process.env.KEY); // ❌ Error: "process" is not allowed
pureServerFn(() => window.location.href); // ❌ Error: "window" is not allowed
Closure variables — referencing outer scope:
// Closure variables — referencing outer scope is not allowed
const SECRET = 'my-secret-key';
pureServerFn((x: string) => x + SECRET); // ❌ Error: Closure variable "SECRET" is not allowed
const config = {maxRetries: 3};
pureServerFn((x: number) => x + config.maxRetries); // ❌ Error: Closure variable "config" is not allowed
Imported or dynamic arguments — cannot be statically analyzed:
// Imported functions cannot be statically analyzed — must be defined in the same file
import {myFn} from './helpers.ts';
pureServerFn(myFn); // ❌ Error: argument "myFn" is imported from another module
pureServerFn({pureFn: myFn}); // ❌ Error: argument "myFn" is imported from another module
import {myFactory} from './helpers.ts';
registerPureFnFactory('ns', 'fn', myFactory); // ❌ Error: argument "myFactory" is imported from another module
Invalid mapFrom mappers:
// mapFrom() mapper follows the same purity rules — closure variables are not allowed
const MULTIPLIER = 5;
mapFrom(someSubRequest, (x: number) => x * MULTIPLIER); // ❌ Error: Closure variable "MULTIPLIER" is not allowed
// Forbidden identifiers not allowed in mapFrom() mapper
mapFrom(someSubRequest, (url: string) => fetch(url)); // ❌ Error: "fetch" is not allowed
// Imported mapper not allowed — must be defined inline
import {myMapper} from './helpers.ts';
mapFrom(someSubRequest, myMapper); // ❌ Error: argument "myMapper" is imported from another module
The @mionkit/pure-functions ESLint rule validates purity at lint time, catching violations before they reach the build step. See the ESLint Rules page for details.