Skip to main content

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

Build docs developers (and LLMs) love