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
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"
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 );
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
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
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"
}
}
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 );
}
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' );
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