Skip to main content

Component Model

TrailBase uses the WebAssembly Component Model, which provides:
  • Language interoperability: Components written in different languages can work together
  • Interface types: Rich type system with records, variants, and resources
  • Composition: Components can import and export interfaces
  • Versioning: Built-in support for API versioning

WIT (WebAssembly Interface Types)

Components are defined using WIT files that specify their interfaces. TrailBase provides the trailbase:component package:
package trailbase:component@0.1.0;

interface init-endpoint {
  record arguments {
    version: option<string>,
  }

  enum http-method-type {
    get, post, head, options, patch, delete, put, trace, connect,
  }

  record http-handlers {
    handlers: list<tuple<http-method-type, string>>,
  }

  init-http-handlers: func(args: arguments) -> http-handlers;

  record job-handlers {
    handlers: list<tuple<string, string>>,
  }

  init-job-handlers: func(args: arguments) -> job-handlers;
}

Creating a Rust Component

1

Set up your project

Create a new library crate:
cargo new --lib my-component
cd my-component
Add dependencies to Cargo.toml:
[lib]
crate-type = ["cdylib"]

[dependencies]
trailbase-wasm = "0.1"
2

Implement the Guest trait

Create your component in src/lib.rs:
use trailbase_wasm::http::{HttpRoute, Request, routing};
use trailbase_wasm::{Guest, export};

struct MyComponent;

impl Guest for MyComponent {
    fn http_handlers() -> Vec<HttpRoute> {
        vec![
            routing::get("/hello", hello_handler),
            routing::post("/data", data_handler),
        ]
    }
}

async fn hello_handler(_req: Request) -> String {
    "Hello from WASM!".to_string()
}

async fn data_handler(mut req: Request) -> Result<String, HttpError> {
    let body = req.body().bytes().await?;
    Ok(format!("Received {} bytes", body.len()))
}

export!(MyComponent);
3

Build the component

Compile to WASM:
cargo build --target wasm32-wasip2 --release
Convert to a component:
wasm-tools component new \
  target/wasm32-wasip2/release/my_component.wasm \
  -o my_component.wasm
4

Deploy to TrailBase

Copy the component to your TrailBase data directory:
cp my_component.wasm /path/to/traildepot/components/
TrailBase will automatically load it on startup or restart.

Creating a TypeScript Component

1

Set up your project

Create a new project:
npm init -y
npm install trailbase-wasm
Configure package.json:
{
  "type": "module",
  "scripts": {
    "build": "node build.js"
  }
}
2

Write your component

Create src/index.ts:
import { defineConfig } from "trailbase-wasm";
import { HttpHandler } from "trailbase-wasm/http";
import { JobHandler } from "trailbase-wasm/job";
import { query } from "trailbase-wasm/db";

export default defineConfig({
  httpHandlers: [
    HttpHandler.get("/fibonacci", (req) => {
      const n = parseInt(req.getQueryParam("n") || "10");
      return fibonacci(n).toString();
    }),
    
    HttpHandler.get("/users", async (req) => {
      const rows = await query(
        'SELECT * FROM users LIMIT 10',
        []
      );
      return HttpResponse.json(rows);
    }),
  ],
  
  jobHandlers: [
    JobHandler.hourly("cleanup", async () => {
      console.log("Running cleanup job");
      await query('DELETE FROM logs WHERE created < $1', [
        Date.now() - 86400000
      ]);
    }),
  ],
});

function fibonacci(num: number): number {
  return num <= 1 ? num : fibonacci(num - 1) + fibonacci(num - 2);
}
3

Build configuration

Create build.js to compile TypeScript to WASM:
import { build } from 'esbuild';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

// Build TypeScript
await build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  format: 'esm',
  target: 'esnext',
  outfile: 'dist/index.js',
});

// Compile to WASM using wasm-tools
await execAsync('wasm-tools component new dist/index.wasm -o component.wasm');
4

Build and deploy

npm run build
cp component.wasm /path/to/traildepot/components/

Creating a JavaScript Component

JavaScript components use the same approach as TypeScript but without type annotations.
import { defineConfig } from "trailbase-wasm";
import { HttpHandler } from "trailbase-wasm/http";
import { query } from "trailbase-wasm/db";

export default defineConfig({
  httpHandlers: [
    HttpHandler.get("/hello", (req) => {
      const name = req.getQueryParam("name") || "World";
      return `Hello, ${name}!`;
    }),
    
    HttpHandler.post("/data", async (req) => {
      const data = req.json();
      await query(
        'INSERT INTO events (data) VALUES ($1)',
        [JSON.stringify(data)]
      );
      return HttpResponse.json({ success: true });
    }),
  ],
});

Component Initialization

Components implement the Guest trait with three optional methods:
pub trait Guest {
    /// Called once when the component is loaded
    fn init(_args: Args) {}

    /// Register HTTP request handlers
    fn http_handlers() -> Vec<HttpRoute> {
        vec![]
    }

    /// Register scheduled job handlers
    fn job_handlers() -> Vec<Job> {
        vec![]
    }

    /// Register custom SQLite scalar functions
    fn sqlite_scalar_functions() -> Vec<SqliteFunction> {
        vec![]
    }
}

Request Handling

HTTP Routes

Define routes with path parameters:
routing::get("/users/{id}", async |req| {
    let id = req.path_param("id")
        .ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
    
    let rows = query(
        "SELECT * FROM users WHERE id = $1",
        [Value::Text(id.to_string())]
    ).await?;
    
    Ok(Json(rows))
})

Query Parameters

async fn search_handler(req: Request) -> Result<Json<Vec<User>>, HttpError> {
    #[derive(Deserialize)]
    struct Query {
        q: String,
        limit: Option<i64>,
    }
    
    let query: Query = req.query_parse()?;
    
    let rows = query(
        "SELECT * FROM users WHERE name LIKE $1 LIMIT $2",
        [Value::Text(format!("%{}%", query.q)), Value::Integer(query.limit.unwrap_or(10))]
    ).await?;
    
    Ok(Json(rows))
}

Response Types

Multiple response types are supported:
// Plain text
async fn text_handler(_req: Request) -> String {
    "Hello!".to_string()
}

// JSON
use trailbase_wasm::http::Json;

async fn json_handler(_req: Request) -> Json<MyData> {
    Json(MyData { value: 42 })
}

// HTML
use trailbase_wasm::http::Html;

async fn html_handler(_req: Request) -> Html<String> {
    Html("<h1>Hello</h1>".to_string())
}

// Redirect
use trailbase_wasm::http::Redirect;

async fn redirect_handler(_req: Request) -> Redirect {
    Redirect::to("/new-location")
}

// Custom Response
use trailbase_wasm::http::Response;

async fn custom_handler(_req: Request) -> Response {
    Response::builder()
        .status(StatusCode::CREATED)
        .header("X-Custom", "value")
        .body("Created".into_body())
        .unwrap()
}

Error Handling

use trailbase_wasm::http::{HttpError, StatusCode};

async fn handler(req: Request) -> Result<String, HttpError> {
    let id = req.path_param("id")
        .ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
    
    let rows = query("SELECT * FROM users WHERE id = $1", [Value::Text(id.to_string())])
        .await
        .map_err(|err| HttpError::message(
            StatusCode::INTERNAL_SERVER_ERROR,
            err.to_string()
        ))?;
    
    if rows.is_empty() {
        return Err(HttpError::status(StatusCode::NOT_FOUND));
    }
    
    Ok(format!("Found user: {}", id))
}

Hot Reloading

In development mode, TrailBase can watch for component changes and reload them automatically.
Create a watcher script:
// hot-reload.ts
import { watch } from 'fs';
import { exec } from 'child_process';

watch('src', { recursive: true }, (eventType, filename) => {
  console.log(`File changed: ${filename}`);
  exec('npm run build', (err, stdout, stderr) => {
    if (err) {
      console.error(stderr);
    } else {
      console.log('Rebuilt successfully');
    }
  });
});

Next Steps

Custom Endpoints

Build HTTP endpoints with WASM

Server-Side Rendering

Render HTML dynamically

Jobs Scheduler

Create scheduled tasks

WASM Overview

Learn about the runtime

Build docs developers (and LLMs) love