Skip to content

Getting Started

Welcome! This guide walks you through using Transit step by step. Transit lets your JavaScript code call functions written in Rust, Python, and Java — like they are all one language.

Imagine you have three friends who speak different languages: one speaks Rust (fast at math), one speaks Python (great at data tasks), and one speaks Java (handles big jobs). Transit is a translator that lets you — the JavaScript speaker — talk to all three at once, seamlessly.

You write your code in JavaScript. When you need something fast, you call a Rust function. When you need something simple, you call a Python function. Transit handles all the communication behind the scenes.

You will need some tools installed on your computer. Here is how to check:

Tool What it does How to check if you have it
Node.js (version 20 or higher) Runs JavaScript Open a terminal and type node --version
bun or npm Installs packages Type bun --version or npm --version
Rust Only if you want Rust functions Type rustc --version
Python 3.10+ Only if you want Python functions Type python3 --version
Java JDK 21+ Only if you want Java functions Type java --version
C/C++ compiler (GCC, Clang, or MSVC) Only if you want C/C++ functions Type gcc --version or g++ --version

If you only want Rust and Python, you can skip Java and C/C++ entirely. Transit only loads what you ask it to.

Step 0: Initialize Your Project (Optional)

Section titled “Step 0: Initialize Your Project (Optional)”

Transit can scan your project and create a config file for you:

Terminal window
mkdir my-transit-app
cd my-transit-app
bun init -y
bun install transit
npx transit init

transit init scans your project for transit.rust(), transit.java(), transit.python(), transit.c(), and transit.cpp() calls, detects which languages you are using, and creates transit.config.json. It is idempotent — safe to run multiple times.

If you prefer to set things up manually, skip this step and continue below.

If you did not run transit init, set up manually:

Terminal window
mkdir my-transit-app
cd my-transit-app
bun init -y
bun install transit

Rust is great for fast, safe code. Let us create a simple Rust function.

First, make the folders and files:

Terminal window
mkdir -p rust/src

Now create the file rust/Cargo.toml (this tells Rust how to build your code). Put this inside:

[package]
name = "my-rust-module"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
napi = { version = "3", features = ["serde-json"] }
napi-derive = "3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[build-dependencies]
napi-build = "2"

Create rust/build.rs (required by napi-rs):

fn main() {
napi_build::setup();
}

Important: The crate-type = ["cdylib", "lib"] and build.rs are required for the Rust bridge to work. Without them, your functions will be discovered by the scanner but will fail at runtime because no native addon is created.

Create the file rust/src/lib.rs and put this inside:

use napi_derive::napi;
// This function takes a name and returns a greeting
#[napi]
pub fn greet(name: String) -> String {
format!("Hello from Rust, {}!", name)
}
// This function adds two numbers
#[napi]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

What this does:

  • #[napi] tells Transit “please export this function so JavaScript can call it” — this is required, not just pub fn
  • pub fn means “this is a public function”
  • The function takes inputs and returns a result, just like any normal function

Note: Without #[napi], the scanner will find your pub fn functions, but the Rust bridge will not be able to call them at runtime. Always use #[napi].

Terminal window
cd rust
cargo build --release

This compiles your Rust code. It will take a minute the first time.

When it finishes, you need to copy the result so Transit can find it:

Terminal window
cp target/release/libmy_rust_module.so index.node
cd ..

Important: On Mac, use cp target/release/libmy_rust_module.dylib index.node instead.

Create the file index.js in your project root:

import { transit } from "transit";
import { resolve } from "node:path";
const __dirname = import.meta.dirname;
// Tell Transit where your Rust code lives
const rs = transit.rust(resolve(__dirname, "./rust"));
// Call the Rust functions — it feels like calling any JavaScript function!
const greeting = await rs.greet("World");
console.log(greeting); // "Hello from Rust, World!"
const sum = await rs.add(10, 20);
console.log(sum); // 30

What this does:

  • transit.rust(...) scans your Rust directory and finds the functions you exported
  • rs.greet("World") calls your Rust function from JavaScript — Transit handles all the communication
  • await is needed because the first call loads the Rust code (subsequent calls are instant)
Terminal window
node index.js

You should see:

Hello from Rust, World!
30

Congratulations! You just called Rust from JavaScript!

Python is great for data processing and simple scripts. Let us add some Python functions.

Transit automatically copies transit_server.py from the Transit package into your Python directory on first use. You can also copy it manually if needed:

Terminal window
mkdir -p python
cp node_modules/@sabeeirsharrma/py-runtime/transit_server.py python/

Note: Transit handles this automatically — the manual copy step is optional.

Create the file python/service.py (or any name you like — Transit will find it):

import json
from transit_server import TransitServer, register_function
def process_data(args_json):
"""Process some data and return a result.
IMPORTANT: args_json is a JSON STRING, not a dictionary!
You must call json.loads() to parse it.
"""
args = json.loads(args_json)
items = args.get("items", [])
return json.dumps({
"output": f"Python processed {len(items)} items",
"processed": True
})
def get_stats(args_json):
"""Return some stats."""
return json.dumps({"status": "healthy", "language": "python"})
if __name__ == "__main__":
server = TransitServer()
register_function("processData", process_data)
register_function("getStats", get_stats)
server.start()

What this does:

  • Each function takes a JSON string (not a dictionary!) and returns a JSON string
  • json.loads(args_json) parses the string into a dictionary you can work with
  • register_function("processData", process_data) tells Transit “when JavaScript calls processData, run my process_data function”
  • The names do not have to match — you can call the JavaScript name anything you want
  • server.start() starts a tiny server that listens for JavaScript requests

Common mistake: Writing def process_data(args): and calling args.get("items") directly. This will fail because args is a JSON string, not a dictionary. Always use json.loads(args) first.

Update your index.js:

import { transit } from "transit";
import { resolve } from "node:path";
const __dirname = import.meta.dirname;
const rs = transit.rust(resolve(__dirname, "./rust"));
const py = transit.python(resolve(__dirname, "./python"));
// Call Rust
console.log(await rs.greet("Transit"));
// Call Python
const result = await py.processData({ items: [1, 2, 3] });
console.log(result); // {"output": "Python processed 3 items", "processed": true}

Run it:

Terminal window
node index.js

Note: The first time you call a Python function, Transit starts a Python process in the background. This takes a moment. After that, all calls are fast.

If your Python file is not named service.py, main.py, app.py, server.py, or transit_service.py, tell Transit which file to use:

const py = transit.python(resolve(__dirname, "./python"), { serverScript: "filters.py" });

Transit looks for entry points in this order: transit_service.py, service.py, main.py, app.py, server.py, filters.py.

Java is good for complex business logic. Let us add some Java.

Create java/src/main/java/com/example/App.java:

package com.example;
import transit.java.TransitServer;
public class App {
public String processJob(String argsJson) {
return "{\"output\": \"Java processed the job\"}";
}
public String getVersion(String argsJson) {
return "{\"version\": \"1.0.0\"}";
}
public static void main(String[] args) throws Exception {
TransitServer server = new TransitServer();
App app = new App();
server.registerFunction("processJob", app::processJob);
server.registerFunction("getVersion", app::getVersion);
server.start();
}
}

What this does:

  • Each Java method takes a String (JSON) and returns a String (JSON)
  • server.registerFunction("processJob", app::processJob) links the JavaScript name to the Java method
  • The main method starts the Java server

You need to compile the Java code. This requires the Java Development Kit (JDK). Transit automatically copies the Java runtime sources (TransitServer.java, TransitService.java, BinaryProtocol.java) from the npm package into your project on first use:

Terminal window
mkdir -p java/build
# Build the Transit Java runtime (auto-copied by Transit, or copy manually)
javac -d java/build java/lib/transit/java/*.java
# Build your app
javac -cp java/build -d java/build java/src/main/java/com/example/*.java

Update index.js:

import { transit } from "transit";
import { resolve } from "node:path";
const __dirname = import.meta.dirname;
const rs = transit.rust(resolve(__dirname, "./rust"));
const jv = transit.java(resolve(__dirname, "./java/src/main/java"));
const py = transit.python(resolve(__dirname, "./python"));
// Call all three languages — they all feel the same from JavaScript!
console.log(await rs.greet("World"));
console.log(await jv.processJob({}));
console.log(await py.processData({ items: [1, 2, 3] }));

If Transit cannot find your compiled Java classes, specify the classpath and main class explicitly:

const jv = transit.java(resolve(__dirname, "./java"), {
classpath: resolve(__dirname, "./java/build"),
mainClass: "com.example.App"
});

Add this to your index.js:

transit.info();

This prints every function Transit discovered:

rust (./rust): 2 functions
- greet [tier 1] (pub fn greet(name: String) -> String)
- add [tier 1] (pub fn add(a: i32, b: i32) -> i32)
python (./python): 2 functions
- processData [tier 1] (def process_data(args_json))
- getStats [tier 1] (def get_stats(args_json))
java (./java/src/main/java): 2 functions
- processJob [tier 1] (public String processJob(String argsJson))
- getVersion [tier 1] (public String getVersion(String argsJson))

Transit has two modes:

Mode Command What happens
Dev mode node index.js or transit dev Uses a Proxy to dynamically dispatch function calls. Great for development — no build step needed.
Build mode transit build Generates typed TypeScript stubs and compiles native addons. Better for production — gives you type safety and faster startup.

In dev mode (the default), Transit uses a JavaScript Proxy to intercept function calls and route them to the right bridge. This means you can call functions immediately without any code generation.

In build mode, Transit generates explicit TypeScript functions for each discovered function. This gives you autocomplete, type checking, and slightly faster startup.

For most projects, dev mode is fine. Use build mode when you want type safety or are deploying to production.

You do not need to do anything special to export functions. Transit finds them automatically:

  • Rust: Any function with #[napi] and pub fn is found
  • Python: Any top-level def function (not starting with _) is found
  • Java: Any public method is found
  • C: Any function matching the generated transit_c_glue.gen.h signatures is found
  • C++: Any function matching the generated transit_cpp_glue.gen.h signatures is found
  • JavaScript: Any export function is found

If you have two files with the same function name, use this syntax:

await rs["lib"]["process"](data) // calls lib.rs's process
await rs["utils"]["process"](data) // calls utils.rs's process

Here is what a typical Transit project looks like:

my-project/
rust/
src/lib.rs # Your Rust functions
Cargo.toml
build.rs # Required for napi-rs
python/
transit_server.py # Copied from Transit package
service.py # Your Python functions
java/
src/main/java/... # Your Java functions
build/ # Compiled classes
c/
src/addon.c # Your C functions
binding.gyp # node-gyp build config
cpp/
src/addon.cpp # Your C++ functions
binding.gyp # node-gyp build config
index.js # Your JavaScript entry point
transit.config.json # Optional: transit init creates this

“Scanner not available” or “native addon not built”

The scanner is the tool that finds your functions. Build it first:

Terminal window
cd node_modules/@sabeeirsharrma/scanner
cargo build --release
cp target/release/libtransit_scanner.so index.node
cd ../..

On Mac: cp target/release/libtransit_scanner.dylib index.node

Bun users: If you see this error with Bun, you may need to create a symlink:

Terminal window
ln -sf transit-scanner.node node_modules/@sabeeirsharrma/scanner/index.node

“Python entry point not found”

Make sure your Python file is named one of: transit_service.py, service.py, main.py, app.py, server.py, or filters.py. Or specify a custom entry point:

const py = transit.python("./python", { serverScript: "my_file.py" });

Also make sure transit_server.py is in the same directory. Transit auto-copies it from the Transit package, but you can also copy it manually:

Terminal window
cp node_modules/@sabeeirsharrma/py-runtime/transit_server.py python/

“Java class not found”

Compile your Java code and specify the classpath:

const jv = transit.java("./java", {
classpath: resolve(__dirname, "./java/build"),
mainClass: "com.example.App"
});

Functions not showing up

Check that your functions match these rules:

  • Rust: must have #[napi] annotation AND be pub fn (not just pub fn alone)
  • Python: must be def at the top level (not inside a class unless you want ClassName.method style)
  • Java: must be public methods
  • JavaScript: must be export function

The first Python/Java call is slow

This is normal! The first call starts the Python/Java process. After that, all calls are fast because the process stays running.

“Port already in use”

If you see this error, another Transit process might be running. Kill it:

Terminal window
pkill -f "transit_service.py"