Getting Started with NimTS
Welcome to NimTS, a bridge generator connecting Nim to TypeScript and WebAssembly (WASM).
NimTS enables writing high-performance WebAssembly libraries in Nim with natural Nim signatures, companion C-ABI entry points, automatic .d.ts declaration generation, and modern TypeScript wrappers.
Prerequisites
Before using NimTS, ensure your environment has the following installed:
- Nim (version 2.0.0 or later, tested with 2.2+)
- Node.js (version 20.x or later) and npm
- LLVM & WebAssembly Toolchain:
- macOS:bash
brew install llvm wasi-sysroot - Ubuntu / Debian:bash
sudo apt-get install -y clang lld llvm # Download wasi-sysroot WASI_VERSION=22 wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_VERSION}/wasi-sysroot-${WASI_VERSION}.0.tar.gz sudo tar -xzf wasi-sysroot-${WASI_VERSION}.0.tar.gz -C /usr/share/ export WASI_SYSROOT=/usr/share/wasi-sysroot
- macOS:
Installation
In Your Nim Project (Nimble)
Add nimts to your .nimble file or install directly:
nimble install nimtsIn Your TypeScript / Frontend Project (npm)
npm install @axiomantic/nimtsTIP
Which do you need?
- Full-Stack / Vite App (Most Common): You will use both.
nimble install nimtsinstalls the Nim macros, types, and compiler tools;npm install @axiomantic/nimtsprovides the Vite plugin (@axiomantic/nimts/vite) and runtime loader (@axiomantic/nimts/runtime). - Nim Library Author: If you are writing a Nim library that generates
.wasmand.d.tsbundles for JavaScript users, you neednimble install nimtsduring development. When ready, you runnpm publishto publish the resulting JavaScript/TypeScript artifacts to npm. - Pure TypeScript Consumer: If you are consuming a pre-compiled NimTS package published by someone else on npm, you only need
npm install <their-package>.
Quickstart: Vite Integration (Recommended)
The fastest and most modern way to build with NimTS is using the official Vite plugin. You import your .nim modules directly into your frontend code—Vite handles background WASM compilation, type generation, and hot module reloading automatically.
Step 1: Configure Vite
Add the nimtsPlugin to your vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
import { nimtsPlugin } from '@axiomantic/nimts/vite';
export default defineConfig({
plugins: [
nimtsPlugin()
]
});Step 2: Write Your Nim Module
Create src/synth.nim:
# src/synth.nim
import std/asyncdispatch
import nimts
type
AudioEngine* {.exportts.} = ref object
sampleRate*: float32
volume*: float32
# 1. Primitives: passed & returned directly via registers
proc addNumbers*(a: int32, b: int32): int32 {.exportts.} =
a + b
# 2. Strings: UTF-8 marshaled automatically
proc greet*(name: string): string {.exportts.} =
"Hello, " & name & " from NimTS!"
# 3. Binary buffers: maps directly to Uint8Array
proc processBuffer*(data: openArray[byte]): seq[byte] {.exportts.} =
result = newSeq[byte](data.len)
for i in 0 ..< data.len:
result[i] = data[i] xor 0xAA'u8
# 4. Stateful handles for ref object instances
proc createEngine*(sampleRate: float32): AudioEngine {.exportts.} =
AudioEngine(sampleRate: sampleRate, volume: 1.0'f32)
proc setVolume*(engine: AudioEngine, vol: float32) {.exportts.} =
if engine != nil:
engine.volume = vol
# 5. Non-blocking async coroutine: Future[T] -> JS Promise<T>
proc computeAsync*(x: int32): Future[int32] {.async, exportts.} =
return x * 42Step 3: Consume in Frontend Code (Direct vs. Worker)
NimTS supports two execution patterns depending on the computational cost of your code:
- Direct In-Thread Mode (
from './synth.nim'): Runs directly on the calling thread (e.g. main UI thread or AudioWorklet). Best for instant synchronous math (< 1ms), real-time canvas rendering, or low-latency state mutations. - Off-Thread Web Worker Mode (
from './synth.nim?worker'): Spawns an isolated background Web Worker on a dedicated OS thread. Best for heavy algorithms, physics simulations, audio rendering, or ML inference.
WARNING
WebAssembly Threading & UI Blocking
In WebAssembly, calling a function on the main UI thread executes synchronously on the browser's JavaScript event loop. Even if a Nim procedure returns a Future[T] (bridged to a JS Promise<T>), CPU-intensive computation on the main thread will block the browser window, drop animation frames, and delay user interactions. For computationally intensive workloads, use the ?worker syntax to run on a background thread.
Example: Consuming Both Patterns (src/main.ts)
You can import both patterns in the same application—or even the same file—depending on your performance needs:
// src/main.ts
// =========================================================================
// Pattern 1: Direct in-thread import (Synchronous, instant, zero overhead)
// =========================================================================
import {
initNimTs,
addNumbers,
greet,
processBuffer,
createEngine,
setVolume
} from './synth.nim';
// =========================================================================
// Pattern 2: Off-thread Web Worker import (True parallel background thread)
// =========================================================================
import SynthWorker from './synth.nim?worker';
// -------------------------------------------------------------------------
// 1. Direct In-Thread Execution (Sub-nanosecond register speed)
// Use when: Operations take < 1ms, or updating real-time canvas/DOM state.
// CAUTION: Heavy computations here WILL freeze the browser window!
// -------------------------------------------------------------------------
await initNimTs();
// Instant synchronous calls directly into WASM registers
console.log('Direct sync result:', addNumbers(17, 25)); // 42
console.log(greet("TypeScript")); // "Hello, TypeScript from NimTS!"
// Direct binary buffer manipulation (Uint8Array)
const input = new Uint8Array([0x01, 0x02, 0x03]);
const output = processBuffer(input); // Uint8Array [ 171, 168, 169 ]
// Stateful handles and direct memory access
const engine = createEngine(48000.0);
setVolume(engine, 0.707);
engine.dispose(); // Explicit deterministic cleanup
// -------------------------------------------------------------------------
// 2. Off-Thread Web Worker Execution (Dedicated background OS thread)
// Use when: Audio processing, physics simulations, ONNX inference, heavy loops.
// Keeps the main UI thread running at 60/120 FPS with zero frame drops.
// -------------------------------------------------------------------------
const worker = await SynthWorker.spawn();
// Calls the exact same Nim function off-thread; returns a Promise
const workerSum = await worker.addNumbers(17, 25);
console.log('Worker async result:', workerSum); // 42
// Off-thread string and buffer operations
const greeting = await worker.greet('Worker Thread');
console.log(greeting);
// Graceful cleanup
worker.terminate();Alternative: Building the Frontend in Nim (nim js)
If you write your browser frontend in Nim using its JavaScript backend (nim js with std/dom or frameworks like Karax), you don't need TypeScript at all!
NimTS provides a client module (nimts/js_client) that allows your nim js frontend to load and call your high-performance nimts WASM module with native Nim signatures, supporting both synchronous direct calls and asynchronous await operations:
# src/frontend.nim
# Compile with: nim js -d:release -o:public/app.js src/frontend.nim
import std/[dom, asyncjs]
import nimts/js_client
# 1. Define the typed interface matching the WASM module's exports
type SynthExports = ref object
addNumbers*: proc(a, b: int32): int32 {.nimcall.}
greet*: proc(name: cstring): cstring {.nimcall.}
computeAsync*: proc(x: int32): Future[int32] {.nimcall.}
# 2. Main DOM entry point
proc main() {.async.} =
# Load and initialize the WASM module with typed exports
let wasm = await loadNimTsExportsTyped[SynthExports]("/wasm/synth.js", "/wasm/synth.wasm")
let btnSync = document.getElementById("calc-sync-btn")
let btnAsync = document.getElementById("calc-async-btn")
let output = document.getElementById("result-display")
# -------------------------------------------------------------------------
# 1. Direct Synchronous Execution (Sub-nanosecond register speed)
# -------------------------------------------------------------------------
btnSync.addEventListener("click", proc(e: Event) =
# Instant synchronous call directly into WASM registers
let total = wasm.addNumbers(17, 25)
output.textContent = "Sync result from WASM: " & $total
)
# -------------------------------------------------------------------------
# 2. Asynchronous Execution with await (std/asyncjs)
# -------------------------------------------------------------------------
btnAsync.addEventListener("click", proc(e: Event) =
# Asynchronous call awaiting WASM result without blocking UI rendering
discard (proc() {.async.} =
output.textContent = "Computing async in WASM..."
let total = await wasm.computeAsync(42)
output.textContent = "Async result from WASM: " & $total
)()
)
discard main()This Dual-Tier Nim Architecture gives you the best of both worlds:
nim jsTier: Full, lightweight access to the browser DOM, Web APIs, and UI components without any bundle bloat.nimtsWASM Tier: Unrestricted 64-bit math, SIMD vectorization, WebGPU-aligned memory buffers, and raw C-speed performance.
For an in-depth breakdown of nim js limitations and architectural best practices, see the Dual-Tier Nim Architecture Guide.
Advanced Usage: Standalone CLI & Manual Compilation
If you are building outside of Vite (e.g. headless Node.js services, standalone CLI utilities, embedded systems, or custom build scripts), you can compile modules manually with the standalone nimts CLI tool.
1. Emit Glue Code at Compile Time
Add an emitTsGlueFiles statement to the bottom of your Nim file:
static:
# Emits synth.d.ts and synth.js glue code at compile-time
emitTsGlueFiles("synth", ".")2. Compile to WebAssembly (Choosing Your Backend)
Compile the Nim file into a WebAssembly binary. NimTS supports two compilation backends:
- Standalone Micro-WASM Backend (Default): Uses LLVM Clang,
wasm-ld, andwasi-sysroot. Produces minimal, zero-bloat WASM binaries (~50–250 kB) ideal for AudioWorklets, Web Workers, and lightweight web apps. - Emscripten Runtime Backend: Uses
emccwith a virtual filesystem (FS), POSIX libc shims, and modularized ES6 output. Ideal when your code wraps third-party C libraries expecting file I/O or full POSIX support.
# 1. Standalone micro-WASM (Default)
nimts compile synth.nim synth.wasm
# 2. Emscripten Runtime Backend
nimts compile synth.nim synth.wasm --backend:emscriptenThis generates:
synth.wasm: WebAssembly binary.synth.d.ts: Full TypeScript type declarations for all exported types and functions.synth.js: Modern ES module with memory allocation, handle tracking, and promise bridging (identical TypeScript interface across both backends).
3. Consume the Generated ES Module
// app.ts
import { initNimTs, addNumbers } from './synth.js';
await initNimTs();
console.log(addNumbers(17, 25));What's Next?
- Explore Core Concepts to understand how
{.exportts.}preserves native signatures without overhead. - Learn about Type Marshaling for numbers, strings, byte arrays, and value objects.
- Read how Stateful Handles & GC manage
ref objectlifecycles safely. - Check out the Vite Integration to import
.nimfiles directly without manual compilation steps.