Overview
TrailBase allows you to create custom HTTP endpoints using WebAssembly components. These endpoints can handle any HTTP method, process request data, interact with the database, and return various response types.Creating HTTP Endpoints
Basic Endpoint
use trailbase_wasm::http::{HttpRoute, Request, routing};
use trailbase_wasm::{Guest, export};
struct MyEndpoints;
impl Guest for MyEndpoints {
fn http_handlers() -> Vec<HttpRoute> {
vec![
routing::get("/api/hello", hello_handler),
routing::post("/api/data", data_handler),
routing::patch("/api/update", update_handler),
routing::delete("/api/remove", delete_handler),
]
}
}
async fn hello_handler(_req: Request) -> String {
"Hello from custom endpoint!".to_string()
}
export!(MyEndpoints);
Path Parameters
Extract dynamic segments from the URL path:routing::get("/users/{id}/posts/{post_id}", async |req| {
let user_id = req.path_param("id")
.ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
let post_id = req.path_param("post_id")
.ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
let rows = query(
"SELECT * FROM posts WHERE user_id = $1 AND id = $2",
[Value::Text(user_id.to_string()), Value::Text(post_id.to_string())]
).await?;
Ok(Json(rows))
})
Query Parameters
Access URL query string parameters:use serde::Deserialize;
#[derive(Deserialize)]
struct SearchQuery {
q: String,
limit: Option<i64>,
offset: Option<i64>,
}
async fn search_handler(req: Request) -> Result<Json<Vec<Row>>, HttpError> {
// Parse query string into struct
let query: SearchQuery = req.query_parse()?;
// Or get individual parameters
let sort = req.query_param("sort").unwrap_or("asc");
let rows = query(
"SELECT * FROM items WHERE name LIKE $1 ORDER BY created LIMIT $2 OFFSET $3",
[
Value::Text(format!("%{}%", query.q)),
Value::Integer(query.limit.unwrap_or(10)),
Value::Integer(query.offset.unwrap_or(0)),
]
).await?;
Ok(Json(rows))
}
Request Body
Process request body data:use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct UserResponse {
id: i64,
name: String,
email: String,
}
async fn create_user_handler(mut req: Request) -> Result<Json<UserResponse>, HttpError> {
// Parse JSON body
let user: CreateUser = req.body().json().await
.map_err(|_| HttpError::status(StatusCode::BAD_REQUEST))?;
// Or get raw bytes
let bytes = req.body().bytes().await
.map_err(|_| HttpError::status(StatusCode::BAD_REQUEST))?;
let result = execute(
"INSERT INTO users (name, email) VALUES ($1, $2)",
[Value::Text(user.name.clone()), Value::Text(user.email.clone())]
).await?;
Ok(Json(UserResponse {
id: result as i64,
name: user.name,
email: user.email,
}))
}
Headers
Access and set HTTP headers:use trailbase_wasm::http::{Response, StatusCode, header};
async fn auth_handler(req: Request) -> Result<Response, HttpError> {
// Read request headers
let auth_header = req.header("authorization")
.ok_or_else(|| HttpError::status(StatusCode::UNAUTHORIZED))?;
let token = auth_header.to_str()
.map_err(|_| HttpError::status(StatusCode::BAD_REQUEST))?;
// Validate token...
// Build response with custom headers
let response = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/json")
.header("X-Request-Id", "123")
.body(r#"{"status":"ok"}"#.into_body())
.unwrap();
Ok(response)
}
Response Types
JSON Response
use trailbase_wasm::http::Json;
#[derive(Serialize)]
struct ApiResponse {
status: String,
data: Vec<String>,
}
async fn json_handler(_req: Request) -> Json<ApiResponse> {
Json(ApiResponse {
status: "success".to_string(),
data: vec!["item1".to_string(), "item2".to_string()],
})
}
HTML Response
use trailbase_wasm::http::Html;
async fn page_handler(_req: Request) -> Html<String> {
let html = r#"
<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>
<h1>Welcome</h1>
<p>Generated by WASM</p>
</body>
</html>
"#;
Html(html.to_string())
}
Redirect Response
use trailbase_wasm::http::Redirect;
async fn redirect_handler(_req: Request) -> Redirect {
// 303 See Other (default)
Redirect::to("/new-location")
}
async fn permanent_redirect(_req: Request) -> Redirect {
// 308 Permanent Redirect
Redirect::permanent("/moved-permanently")
}
async fn temporary_redirect(_req: Request) -> Redirect {
// 307 Temporary Redirect
Redirect::temporary("/temp-location")
}
Binary Response
async fn image_handler(_req: Request) -> Response {
let image_data: Vec<u8> = load_image();
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "image/png")
.body(image_data.into_body())
.unwrap()
}
Database Integration
Endpoints can query and modify the database:use trailbase_wasm::db::{query, execute, Value, Transaction};
// Simple query
async fn list_handler(_req: Request) -> Result<Json<Vec<Row>>, HttpError> {
let rows = query("SELECT * FROM users LIMIT 10", [])
.await
.map_err(|err| HttpError::message(
StatusCode::INTERNAL_SERVER_ERROR,
err.to_string()
))?;
Ok(Json(rows))
}
// Insert/Update/Delete
async fn update_handler(mut req: Request) -> Result<String, HttpError> {
let data: UpdateData = req.body().json().await?;
let affected = execute(
"UPDATE users SET name = $1 WHERE id = $2",
[Value::Text(data.name), Value::Integer(data.id)]
).await?;
Ok(format!("Updated {} rows", affected))
}
// Transaction
async fn transfer_handler(mut req: Request) -> Result<Json<TransferResult>, HttpError> {
let transfer: Transfer = req.body().json().await?;
let mut tx = Transaction::begin()
.map_err(|err| HttpError::message(StatusCode::INTERNAL_SERVER_ERROR, err))?;
tx.execute(
"UPDATE accounts SET balance = balance - $1 WHERE id = $2",
[Value::Real(transfer.amount), Value::Integer(transfer.from_id)]
)?;
tx.execute(
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
[Value::Real(transfer.amount), Value::Integer(transfer.to_id)]
)?;
tx.commit()?;
Ok(Json(TransferResult { success: true }))
}
External API Integration
Make HTTP requests to external services:use trailbase_wasm::fetch::{fetch, get, Request as FetchRequest};
async fn proxy_handler(req: Request) -> Result<Vec<u8>, HttpError> {
let query = req.query_param("q").unwrap_or("rust");
// Simple GET request
let data = get(format!("https://api.github.com/search/repositories?q={}", query))
.await
.map_err(|err| HttpError::message(
StatusCode::BAD_GATEWAY,
err.to_string()
))?;
Ok(data)
}
async fn post_to_external(mut req: Request) -> Result<String, HttpError> {
let body = req.body().bytes().await?;
let request = FetchRequest::builder()
.uri("https://api.example.com/data")
.method("POST")
.header("Content-Type", "application/json")
.body(body.into_body())
.unwrap();
let response = fetch(request).await
.map_err(|err| HttpError::message(StatusCode::BAD_GATEWAY, err))?;
Ok(String::from_utf8_lossy(&response).to_string())
}
Error Handling
use trailbase_wasm::http::{HttpError, StatusCode};
async fn safe_handler(req: Request) -> Result<Json<Data>, HttpError> {
// Validate input
let id = req.path_param("id")
.ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
let parsed_id: i64 = id.parse()
.map_err(|_| HttpError::message(
StatusCode::BAD_REQUEST,
"Invalid ID format"
))?;
// Query database
let rows = query("SELECT * FROM items WHERE id = $1", [Value::Integer(parsed_id)])
.await
.map_err(|err| HttpError::message(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Database error: {}", err)
))?;
// Check results
if rows.is_empty() {
return Err(HttpError::message(
StatusCode::NOT_FOUND,
"Item not found"
));
}
Ok(Json(Data::from_rows(rows)))
}
Authentication
Access authenticated user information:async fn protected_handler(req: Request) -> Result<Json<UserData>, HttpError> {
// Get authenticated user from request context
let user = req.user()
.ok_or_else(|| HttpError::status(StatusCode::UNAUTHORIZED))?;
// User contains: id, email, verified, provider_id, etc.
println!("User ID: {}, Email: {:?}", user.id, user.email);
let data = query(
"SELECT * FROM user_data WHERE user_id = $1",
[Value::Text(user.id.to_string())]
).await?;
Ok(Json(UserData::from_rows(data)))
}
Example: Complete CRUD API
use trailbase_wasm::http::{HttpRoute, Request, Json, StatusCode, routing};
use trailbase_wasm::db::{query, execute, Value};
use trailbase_wasm::{Guest, export};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateTodo {
title: String,
completed: bool,
}
#[derive(Serialize)]
struct Todo {
id: i64,
title: String,
completed: bool,
}
struct TodoApi;
impl Guest for TodoApi {
fn http_handlers() -> Vec<HttpRoute> {
vec![
routing::get("/todos", list_todos),
routing::get("/todos/{id}", get_todo),
routing::post("/todos", create_todo),
routing::patch("/todos/{id}", update_todo),
routing::delete("/todos/{id}", delete_todo),
]
}
}
async fn list_todos(_req: Request) -> Result<Json<Vec<Todo>>, HttpError> {
let rows = query("SELECT id, title, completed FROM todos ORDER BY id", [])
.await?;
let todos = rows.into_iter().map(|row| {
Todo {
id: row[0].as_integer().unwrap(),
title: row[1].as_text().unwrap().to_string(),
completed: row[2].as_integer().unwrap() == 1,
}
}).collect();
Ok(Json(todos))
}
async fn get_todo(req: Request) -> Result<Json<Todo>, HttpError> {
let id: i64 = req.path_param("id")
.and_then(|s| s.parse().ok())
.ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
let rows = query(
"SELECT id, title, completed FROM todos WHERE id = $1",
[Value::Integer(id)]
).await?;
if rows.is_empty() {
return Err(HttpError::status(StatusCode::NOT_FOUND));
}
let row = &rows[0];
Ok(Json(Todo {
id: row[0].as_integer().unwrap(),
title: row[1].as_text().unwrap().to_string(),
completed: row[2].as_integer().unwrap() == 1,
}))
}
async fn create_todo(mut req: Request) -> Result<Json<Todo>, HttpError> {
let todo: CreateTodo = req.body().json().await?;
let id = execute(
"INSERT INTO todos (title, completed) VALUES ($1, $2) RETURNING id",
[Value::Text(todo.title.clone()), Value::Integer(todo.completed as i64)]
).await? as i64;
Ok(Json(Todo {
id,
title: todo.title,
completed: todo.completed,
}))
}
async fn update_todo(mut req: Request) -> Result<Json<Todo>, HttpError> {
let id: i64 = req.path_param("id")
.and_then(|s| s.parse().ok())
.ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
let update: CreateTodo = req.body().json().await?;
execute(
"UPDATE todos SET title = $1, completed = $2 WHERE id = $3",
[
Value::Text(update.title.clone()),
Value::Integer(update.completed as i64),
Value::Integer(id)
]
).await?;
Ok(Json(Todo {
id,
title: update.title,
completed: update.completed,
}))
}
async fn delete_todo(req: Request) -> Result<String, HttpError> {
let id: i64 = req.path_param("id")
.and_then(|s| s.parse().ok())
.ok_or_else(|| HttpError::status(StatusCode::BAD_REQUEST))?;
let affected = execute(
"DELETE FROM todos WHERE id = $1",
[Value::Integer(id)]
).await?;
if affected == 0 {
return Err(HttpError::status(StatusCode::NOT_FOUND));
}
Ok(format!("Deleted todo {}", id))
}
export!(TodoApi);
Next Steps
Server-Side Rendering
Render dynamic HTML pages
Jobs Scheduler
Schedule background tasks
WASM Components
Learn component development
Vector Search
Implement semantic search