api() — server functions as agent tools#
A server function defined once becomes three things: a validated HTTP endpoint, a typed client stub, and a registered agent tool.
TSsrc/server/shop.api.ts
import { api } from '@janux/server';
import { schema, str, money } from 'janux';
export const pay = api({
description: 'Charge the cart. Irreversible monetary action.',
input: schema({ total: money() }),
output: schema({ orderId: str(), charged: money() }),
guard: 'confirm',
run: ({ input, ctx }) => payments.charge(input.total, ctx.userId),
});The three projections#
- Endpoint.
POST /_janux/api/shop.pay— input validated beforerun, output validated after. Errors return a typed envelope with proper status codes (400 invalid input, 403 forbidden). - Client stub.
import { pay } from '../server/shop.api'in client code compiles (via SWC, at build time) to a ~100-byte fetch stub. Server code, constants and imports never reach the browser bundle. - Agent tool.
api.shop.payappears in the manifest with the same description, schema and guard. Agent calls withguard: 'confirm'produce a server-side proposal;POST /_janux/approve {id}executes it exactly once.
Callable everywhere#
The value returned by api() is directly callable on the server too — SSR sources and other apis can invoke it without HTTP:
sources: {
catalog: source({ query: () => catalog({}) }), // direct call on SSR, fetch stub on the client
},Conventions#
- Files live in
src/server/*.api.ts; the filename is the namespace (shop.api.ts→shop.*). - Only exported
constvalues created withapi()are collected. ctxcarries the authenticated request context (via the server'sctxForhook); agent invocations run under the end user's ctx, never a service identity.
Rule of thumb#
Intents mutate UI state; apis mutate world state. An intent that needs the world calls an api from its run().