> ## Documentation Index
> Fetch the complete documentation index at: https://forst-lang.org/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Call Forst from Node

> Invoke Forst functions from Node with forst generate, @forst/client, forst dev, @forst/sidecar, or a built-in HTTP server in your binary.

Run **`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:

```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
import { Echo } from "./client"; // generated by forst generate

await Echo({ message: "hello" });
```

## Quick start

Follow these steps from a public Forst function to a Node call:

<Steps>
  <Step title="Write a public Forst function">
    Only **exported** functions (capitalized names) are callable from Node.

    ```ft theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    package main

    type EchoRequest = { message: String }

    func Echo(input EchoRequest) {
      return { echo: input.message, timestamp: 42 }
    }
    ```
  </Step>

  <Step title="Generate the client">
    ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    npx forst generate ./src
    ```

    This writes `generated/types.d.ts`, per-file `*.client.ts` modules, and `client/index.ts` with flat exports like `export const Echo`.
  </Step>

  <Step title="Install the runtime">
    ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    npm install @forst/client
    ```

    Import from your generated `client/` folder — not from `@forst/client` directly (that package is the HTTP transport).
  </Step>

  <Step title="Run a server">
    **Development** — hot reload while you edit `.ft` files:

    ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    npx forst dev -root ./src -port 6320
    ```

    **Production** — built-in HTTP server in your compiled binary (see [Built-in HTTP server](#built-in-http-server) below):

    ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    npx forst build -root ./src -o ./out/main.go -- ./src/main.ft
    go build -o ./bin/api ./out/*.go
    ./bin/api
    ```
  </Step>

  <Step title="Set the URL and call">
    ```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    export FORST_BASE_URL=http://127.0.0.1:6321   # built-in server in binary
    # or
    export FORST_BASE_URL=http://localhost:6320    # forst dev
    ```

    ```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    import { Echo } from "./client";

    const result = await Echo({ message: "hello" });
    ```
  </Step>
</Steps>

Repository example: [`examples/in/rfc/embedded-invoke`](https://github.com/forst-lang/forst/tree/main/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 one `go.mod`. A typical host layout:

```text theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
myapp/
  ftconfig.json
  go.mod
  main.ft          # package main — nodert host bootstrap
  bcrypt.ft        # package bcrypt — invoke exports
  types.ft         # package types — shared shapes (no client unless exported)
```

| Command              | Multi-package behavior                                                                |
| -------------------- | ------------------------------------------------------------------------------------- |
| `forst run -root .`  | Compiles entry package plus cross-package invoke companions                           |
| `forst dev -root .`  | Discovers runnable exports in **all** packages under `-root`                          |
| `forst generate .`   | Emits **one client module per Forst package** (`bcrypt.client.ts`, not per file stem) |
| `forst test -root .` | Same `modulecheck` graph as run/dev; ephemeral Go under `.forst/gen/test/`            |

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`](https://github.com/forst-lang/forst/tree/main/examples/in/rfc/node-interop/multi-package-dev). Run `task example:multipackage-dev` from the monorepo root.

```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
forst generate -root ./forst .
forst run -root . -- ./forst/main.ft
```

Generated clients use the **Forst package name** in invoke RPC metadata (`package: "bcrypt"`, `function: "Hash"`).

## How the client picks a server

`createInvokeClient` (used inside generated `client/index.ts`) picks transport automatically:

| You run            | Set env                                | Client behavior                                          |
| ------------------ | -------------------------------------- | -------------------------------------------------------- |
| Nothing            | —                                      | Spawns `forst dev` via `@forst/sidecar` (local dev only) |
| `forst dev`        | `FORST_BASE_URL=http://localhost:6320` | HTTP connect                                             |
| Compiled Go binary | `FORST_BASE_URL=http://127.0.0.1:6321` | HTTP connect, in-process dispatch                        |

**`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](/docs/workflow/dev-server).

### Runtime dev profile (embedded / host mode)

When `server.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:

1. CLI `-entry`
2. `dev.entry` in `ftconfig.json`
3. `forst/main.ft` under `-root`
4. `main.ft` under `-root`

```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
export FORST_BASE_URL=http://127.0.0.1:6321
FORST_HOST_MODE=dev forst dev -export-struct-fields -root .
```

**Hot reload:** with default `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`):

```json theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
"dev": { "hotReload": false, "watch": false }
```

Node runtime binaries import `forst/nodert`. Declare that dependency in **`.forst-gomod/go.mod`** at your project root (monorepo dev example):

```go theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
module myapp/forst

go 1.26.0

replace forst => ../../forst/forst   // monorepo: path to forst Go module
// require forst v0.0.37            // production: published release (no compiler checkout)
```

If any `.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:

```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
npm install @forst/sidecar
```

### Spawn mode (local development)

The sidecar starts **`forst dev`** as a child process. Expose a handler in Forst:

<Tabs>
  <Tab title="Forst">
    ```ft theme={"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    package handlers

    func greet(name: String): String {
    	return "Hello, " + name
    }
    ```
  </Tab>

  <Tab title="Generated Go">
    ```go theme={"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}} theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
    // forst build
    package handlers

    func greet(name string) string {
    	return "Hello, " + name
    }
    ```
  </Tab>
</Tabs>

Call it from Node:

```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
import { ForstSidecar } from "@forst/sidecar";

const sidecar = ForstSidecar({
  mode: "development",
  port: 6320,
  root: "./forst-handlers",
});

await sidecar.start();
const result = await sidecar.invoke("handlers", "greet", { name: "World" });
await sidecar.stop();
```

### Connect mode (CI / monorepo)

Point at an already-running **`forst dev`** or built-in server:

```typescript theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
const sidecar = ForstSidecar({
  mode: "connect",
  port: 6320,
  host: "localhost",
});
```

### Watch and generate

Configure **`watchRoots`** 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: false` in the invoke response
* **`DevServerHttpFailure`**: HTTP errors, with optional parsed server error bodies

After **`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`:

```json theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
{
  "server": {
    "embedded": true,
    "host": "127.0.0.1",
    "port": "6321"
  }
}
```

Then `forst build` emits:

* `main.go` — your transpiled code
* `*_forst_invoke_server.gen.go` — dispatch registry + HTTP server

The companion `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:

```bash theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
curl -s -X POST http://127.0.0.1:6321/invoke \
  -H 'Content-Type: application/json' \
  -d '{"package":"main","function":"Echo","args":[{"message":"hello"}]}'
```

## Generated output

After `forst generate`, expect this layout:

```text theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/languages/forst.json"]}}
generated/
├── types.d.ts           # merged type declarations
└── *.client.ts          # one module per .ft file

client/
├── index.ts             # flat exports + optional ForstClient class
├── package.json         # name: @forst/generated-client
└── types.d.ts           # copy of generated types
```

| Package                   | Role                                                            |
| ------------------------- | --------------------------------------------------------------- |
| `@forst/client`           | Runtime transport (`createInvokeClient`, HTTP vs sidecar spawn) |
| `@forst/generated-client` | Your project stubs (emitted under `client/`)                    |

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`

| Field      | Default                   | Notes                                          |
| ---------- | ------------------------- | ---------------------------------------------- |
| `embedded` | `false`                   | Emit invoke companion file (`server.embedded`) |
| `host`     | `127.0.0.1` when embedded | Binds localhost only                           |
| `port`     | `6321` when embedded      | Avoids `forst dev` default `6320`              |
| `cors`     | `false`                   | Off unless enabled                             |

### Environment variables

| Variable                                  | Purpose                                            |
| ----------------------------------------- | -------------------------------------------------- |
| `FORST_BASE_URL` / `FORST_DEV_URL`        | Invoke server URL (aliases)                        |
| `FORST_INVOKE_URL`                        | Explicit URL override                              |
| `FORST_INVOKE_ENABLED`                    | Enable built-in server at runtime without ftconfig |
| `FORST_INVOKE_HOST` / `FORST_INVOKE_PORT` | Override bind address in deployed binaries         |
| `FORST_BOUNDARY_ROOT`                     | Project root for `.forst/invoke.ready`             |
| `FORST_SKIP_SPAWN`                        | Force HTTP connect (never spawn `forst dev`)       |

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:

| Channel              | Direction                 | Typical endpoint                                    |
| -------------------- | ------------------------- | --------------------------------------------------- |
| App HTTP             | Browser → Node app        | `:6322` (e.g. remix-serve in local forst host mode) |
| Built-in HTTP server | Node → Forst              | `http://127.0.0.1:6321/invoke`                      |
| Nodert host socket   | Forst → legacy JavaScript | `.forst/node.sock`                                  |

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`](https://github.com/forst-lang/forst/tree/main/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](/docs/interop/node/call-javascript#host-mode-shared-memory).

## Export rules

Only **public** functions with no unsatisfied [Providers](/docs/language/providers) appear in the generated client. Functions that need wired dependencies belong in Forst `main` startup — not in `/invoke` from Node. The compiler rejects exports that would fail at runtime.

## Troubleshooting

<Accordion title="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).
</Accordion>

<Accordion title="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`.
</Accordion>

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>

## 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-spawn **`forst 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

Wire **`with`** blocks in Go **`main`** before the server starts. Functions with unsatisfied Providers are excluded from the generated client. See [Providers § Caveats](/docs/language/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`](https://github.com/forst-lang/forst/tree/main/examples/in/rfc/embedded-invoke) (`task example:embedded-invoke:run`)
* Sidecar tests: [`examples/in/rfc/sidecar/tests`](https://github.com/forst-lang/forst/tree/main/examples/in/rfc/sidecar/tests)
* Client integration: [`examples/client-integration/`](https://github.com/forst-lang/forst/tree/main/examples/client-integration)
* Run locally: `task example:sidecar-local`

## Related

<CardGroup cols={2}>
  <Card title="Generate client types" icon="https://mintcdn.com/forst/r5GJChnfkgCSJa-b/icons/typescript.svg?fit=max&auto=format&n=r5GJChnfkgCSJa-b&q=85&s=8a8c0cd7b4bf60c264d51f66d0f52e91" href="/docs/interop/node/generate-types" width="512" height="512" data-path="icons/typescript.svg">
    Emit `.d.ts` and client stubs from Forst sources.
  </Card>

  <Card title="Dev server" icon="server" href="/docs/workflow/dev-server">
    `forst dev` HTTP contract and endpoints.
  </Card>

  <Card title="Call JavaScript from Forst" icon="https://mintcdn.com/forst/r5GJChnfkgCSJa-b/icons/typescript.svg?fit=max&auto=format&n=r5GJChnfkgCSJa-b&q=85&s=8a8c0cd7b4bf60c264d51f66d0f52e91" href="/docs/interop/node/call-javascript" width="512" height="512" data-path="icons/typescript.svg">
    Call legacy `.ts`/`.js` from Forst (nodert, host mode).
  </Card>

  <Card title="CLI reference" icon="terminal" href="/docs/workflow/cli">
    `build`, `run`, `generate`, and `-root`.
  </Card>

  <Card title="Client integration example" icon="code" href="https://github.com/forst-lang/forst/tree/main/examples/client-integration">
    Prisma-like migration patterns in the repository.
  </Card>
</CardGroup>
