Skip to content

Custom APIs

On startup TrailBase will automatically load any WASM component, i.e. *.wasm files, that it finds in <traildepot>/wasm. This can be used to implement arbitrary HTTP APIs with custom handlers.

At this point the documentation is embarrassingly “thin”, take a look at the examples for further context. Some are provided here, more can be found in /examples including project templates:

The following example demonstrates how to:

  • register a parameterized route with {table},
  • query the database,
  • return a string body or HTTP error.
docs/examples/wasm-guest-ts/src/index.ts
import { defineConfig } from "trailbase-wasm";
import { query } from "trailbase-wasm/db";
import { HttpHandler, HttpResponse, StatusCode } from "trailbase-wasm/http";
import type { HttpRequest } from "trailbase-wasm/http";
async function countRecordsHandler(req: HttpRequest): Promise<HttpResponse> {
const table = req.getPathParam("table");
if (!table) {
return HttpResponse.status(
StatusCode.BAD_REQUEST,
`Table not found for '?table=${table}'`,
);
}
const rows = await query(`SELECT COUNT(*) FROM ${table}`, []);
return HttpResponse.text(`count: ${rows[0][0]}`);
}
export default defineConfig({
httpHandlers: [HttpHandler.get("/count/{table}", countRecordsHandler)],
});

Another example reading query parameters and producing a JSON response.

examples/coffee-vector-search/guests/typescript/src/index.ts
import { defineConfig } from "trailbase-wasm";
import { HttpHandler, HttpRequest, HttpResponse } from "trailbase-wasm/http";
import { query } from "trailbase-wasm/db";
async function searchHandler(req: HttpRequest): Promise<HttpResponse> {
// Get the query params from the url, e.g. '/search?aroma=4&acidity=7'.
const aroma = req.getQueryParam("aroma") ?? 8;
const flavor = req.getQueryParam("flavor") ?? 8;
const acid = req.getQueryParam("acidity") ?? 8;
const sweet = req.getQueryParam("sweetness") ?? 8;
// Query the database for the closest match.
const rows = await query(
`SELECT Owner, Aroma, Flavor, Acidity, Sweetness
FROM coffee
ORDER BY vec_distance_L2(
embedding, FORMAT("[%f, %f, %f, %f]", $1, $2, $3, $4))
LIMIT 100`,
[+aroma, +flavor, +acid, +sweet],
);
return HttpResponse.json(rows);
}
export default defineConfig({
httpHandlers: [HttpHandler.get("/search", searchHandler)],
});

The following example demonstrates how to:

  • register an HTTP route,
  • read query parameters,
  • query the database,
  • and return a JSON response or errors.
examples/coffee-vector-search/guests/rust/src/lib.rs
#![forbid(unsafe_code, clippy::unwrap_used)]
#![allow(clippy::needless_return)]
#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)]
use trailbase_wasm::db::{Value, query};
use trailbase_wasm::http::{HttpError, HttpRoute, Json, Request, StatusCode, routing};
use trailbase_wasm::{Guest, export};
type SearchResponse = (String, f64, f64, f64, f64);
async fn search_handler(req: Request) -> Result<Json<Vec<SearchResponse>>, HttpError> {
let (mut aroma, mut flavor, mut acidity, mut sweetness) = (8, 8, 8, 8);
for (param, value) in req.url().query_pairs() {
match param.as_ref() {
"aroma" => aroma = value.parse().unwrap_or(aroma),
"flavor" => flavor = value.parse().unwrap_or(flavor),
"acidity" => acidity = value.parse().unwrap_or(acidity),
"sweetness" => sweetness = value.parse().unwrap_or(sweetness),
_ => {}
}
}
// Query the closest match using vector-search.
let results: Vec<SearchResponse> = query(
r#"
SELECT Owner, Aroma, Flavor, Acidity, Sweetness
FROM coffee
ORDER BY vec_distance_L2(
embedding, FORMAT("[%f, %f, %f, %f]", $1, $2, $3, $4))
LIMIT 100
"#,
[
Value::Integer(aroma),
Value::Integer(flavor),
Value::Integer(acidity),
Value::Integer(sweetness),
],
)
.await
.map_err(|err| HttpError::message(StatusCode::INTERNAL_SERVER_ERROR, err))?
.into_iter()
.map(|row| {
// Convert to json response.
let Value::Text(owner) = row[0].clone() else {
panic!("invariant");
};
return (
owner,
as_real(&row[1]).expect("invariant"),
as_real(&row[2]).expect("invariant"),
as_real(&row[3]).expect("invariant"),
as_real(&row[4]).expect("invariant"),
);
})
.collect();
return Ok(Json(results));
}
fn as_real(v: &Value) -> Result<f64, String> {
return match v {
Value::Real(f) => Ok(*f),
_ => Err(format!("Not a real: {v:?}")),
};
}
// Lastly, implement and export a TrailBase component.
struct GuestImpl;
impl Guest for GuestImpl {
fn http_handlers() -> Vec<HttpRoute> {
return vec![routing::get("/search", search_handler)];
}
}
export!(GuestImpl);