forst generate, import functions from the generated client/, and call them over HTTP from any Node process — Express, Remix loaders, scripts. Point FORST_BASE_URL at a running server.
forst generate emits TypeScript stubs, but the wire protocol is language-agnostic JSON. Examples use .ts because that is what the generator outputs.
A minimal client call:
Quick start
Follow these steps from a public Forst function to a Node call:1
Write a public Forst function
Only exported functions (capitalized names) are callable from Node.
2
Generate the client
generated/types.d.ts, per-file *.client.ts modules, and client/index.ts with flat exports like export const Echo.3
Install the runtime
client/ folder — not from @forst/client directly (that package is the HTTP transport).4
Run a server
Development — hot reload while you edit Production — built-in HTTP server in your compiled binary (see Built-in HTTP server below):
.ft files:5
Set the URL and call
examples/in/rfc/embedded-invoke. Run task example:embedded-invoke:run from the monorepo root.
Multi-package projects
Forst packages are independent compilation units under onego.mod. A typical host layout:
File stems should match package names (
bcrypt.ft → package bcrypt). forst generate errors on mismatch unless you pass --allow-stem-package-mismatch.
Repository example: examples/in/rfc/node-interop/multi-package-dev. Run task example:multipackage-dev from the monorepo root.
package: "bcrypt", function: "Hash").
How the client picks a server
createInvokeClient (used inside generated client/index.ts) picks transport automatically:
FORST_SKIP_SPAWN=1 forces HTTP connect and never auto-spawns forst dev.
The forst dev HTTP contract (/invoke, /version, …) is documented in Dev server.
Runtime dev profile (embedded / host mode)
Whenserver.embedded or node.hostMode is true in ftconfig.json, forst dev compiles and runs the project entry (same pipeline as forst run) instead of the per-invoke executor on :6320. Embedded invoke listens on server.port (default 6321). The CLI -port flag overrides only when you pass it explicitly; omitting -port uses ftconfig.json.
Entry resolution order:
- CLI
-entry dev.entryinftconfig.jsonforst/main.ftunder-rootmain.ftunder-root
ftconfig.json, dev.hotReload is true and forst dev watches .ft files under -root, recompiles on change, and restarts the embedded binary. Set "watch": true explicitly or rely on hotReload. Compile failures are logged to stderr; the watcher stays alive so you can fix errors and save again.
To disable watching and run once (same as forst run):
forst/nodert. Declare that dependency in .forst-gomod/go.mod at your project root (monorepo dev example):
.ft file under -root uses Go FFI (import "golang.org/x/crypto/bcrypt", etc.), declare those modules in .forst-gomod/go.mod and run go mod download or go mod tidy there. Required for embedded invoke discovery across packages in Node-only projects.
forst dev / forst run copy this into the .forst/run sandbox go.mod automatically. With @forst/cli, the shim also wires the cached runtime module internally — you do not set FORST_GOMOD_ROOT in app scripts.
FORST_GOMOD_ROOT is for Forst compiler development only (working on the compiler itself).
Set dev.profile to "executor" to force the legacy sidecar HTTP server even when server.embedded is enabled.
@forst/sidecar
@forst/sidecar is the lower-level Node client for spawn and connect modes. Generated client/ stubs use @forst/client, which may spawn forst dev via the sidecar when no URL is set.
Install in your Node project:
Spawn mode (local development)
The sidecar startsforst dev as a child process. Expose a handler in Forst:
- Forst
- Generated Go
Connect mode (CI / monorepo)
Point at an already-runningforst dev or built-in server:
Watch and generate
ConfigurewatchRoots and optional watchGenerate so the sidecar debounces forst generate after reloads. That keeps generated/types.d.ts aligned while you edit .ft files.
Call generateTypes() with configPath to pass -config through to the compiler.
Error handling
The sidecar client throws typed errors:DevServerInvokeRejected:success: falsein the invoke responseDevServerHttpFailure: HTTP errors, with optional parsed server error bodies
start(), a version check compares /version contractVersion and compiler semver when parseable.
Built-in HTTP server
Your compiled Go binary can listen on localhost (default:6321) and run Forst functions directly when Node sends POST /invoke — no separate forst dev process and no go run per request.
Enable with server.embedded in ftconfig.json:
forst build emits:
main.go— your transpiled code*_forst_invoke_server.gen.go— dispatch registry + HTTP server
init() starts the server. ForstInvokeWaitForShutdown() is appended to main so the process stays alive in long-running deployments.
Same JSON contract as forst dev. Smoke test:
Generated output
Afterforst generate, expect this layout:
Flat exports let you write
import { Echo } from "./client". The optional ForstClient class is there when you want explicit config in the constructor.
Configuration
ftconfig.json — server
Environment variables
When the built-in server starts, Forst writes
.forst/invoke.ready with the server URL for tooling auto-discovery.
Host mode (Remix and similar)
Incremental migration often runs three channels in one deployment:
Enable both
server.embedded and node.hostMode in ftconfig.json. Remix loaders call generated client functions over :6321; Forst main calls legacy JS over the nodert socket.
Full combined demo: examples/in/rfc/node-interop/remix-serve — a todo app with Remix loaders, embedded invoke, and nodert host mode (task example:node-interop-remix-serve:e2e).
Nodert setup and readiness are covered in Call JavaScript from Forst.
Export rules
Only public functions with no unsatisfied Providers appear in the generated client. Functions that need wired dependencies belong in Forstmain startup — not in /invoke from Node. The compiler rejects exports that would fail at runtime.
Troubleshooting
Connection refused on port 6321
Connection refused on port 6321
The compiled binary is not running, or
main exited before the invoke server could accept traffic. Ensure server.embedded is true and the process blocks on ForstInvokeWaitForShutdown() (auto-appended by the compiler).function not found in invoke response
function not found in invoke response
Check the
POST /invoke body: package must match the Forst package name (usually main), and function must match the exported name exactly (Echo, not echo). List available functions with GET /functions.Types do not match the server
Types do not match the server
Re-run
npx forst generate after changing .ft files. In dev, configure watchGenerate on @forst/sidecar to debounce regeneration after reloads.Compile error: function has unsatisfied providers
Compile error: function has unsatisfied providers
Wire providers in Go/
main before the server starts. Provider-dependent functions are intentionally excluded from the generated client surface.Works in dev but not with the compiled binary
Works in dev but not with the compiled binary
Confirm
FORST_BASE_URL points at the built-in server (http://127.0.0.1:6321), not forst dev on 6320. Rebuild the Go binary after changing .ft sources.Caveats
Built-in HTTP server,@forst/client, and @forst/sidecar are experimental. Pin package and compiler versions in production.
Check contractVersion
After upgrades, verify GET /version contractVersion matches what your client expects. @forst/sidecar checks this on start().
Spawn mode is local dev
Auto-spawnforst dev (via @forst/sidecar or @forst/client with no URL) is for local development only. Production should use the built-in HTTP server or explicit connect mode with FORST_BASE_URL.
Exported functions only
Only capitalized function names appear in discovery and the invoke registry.Providers before expose
Wirewith blocks in Go main before the server starts. Functions with unsatisfied Providers are excluded from the generated client. See Providers § Caveats.
Dev vs production URL
Executor profile (forst dev without embedded/host mode) listens on 6320 by default. Runtime profile (embedded or host mode) uses server.port from ftconfig.json (default 6321). A wrong FORST_BASE_URL looks like “works in dev, fails in prod”.
Examples
- Embedded invoke:
examples/in/rfc/embedded-invoke(task example:embedded-invoke:run) - Sidecar tests:
examples/in/rfc/sidecar/tests - Client integration:
examples/client-integration/ - Run locally:
task example:sidecar-local
Related
Generate client types
Emit
.d.ts and client stubs from Forst sources.Dev server
forst dev HTTP contract and endpoints.Call JavaScript from Forst
Call legacy
.ts/.js from Forst (nodert, host mode).CLI reference
build, run, generate, and -root.Client integration example
Prisma-like migration patterns in the repository.