> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/trailbaseio/trailbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom HTTP Endpoints

> Build custom API endpoints with WebAssembly components

## 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

<CodeGroup>
  ```rust Rust theme={null}
  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);
  ```

  ```typescript TypeScript theme={null}
  import { defineConfig } from "trailbase-wasm";
  import { HttpHandler } from "trailbase-wasm/http";

  export default defineConfig({
    httpHandlers: [
      HttpHandler.get("/api/hello", (req) => {
        return "Hello from custom endpoint!";
      }),
      
      HttpHandler.post("/api/data", (req) => {
        const data = req.json();
        return HttpResponse.json({ received: data });
      }),
    ],
  });
  ```
</CodeGroup>

## Path Parameters

Extract dynamic segments from the URL path:

<CodeGroup>
  ```rust Rust theme={null}
  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))
  })
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/users/{id}/posts/{post_id}", async (req) => {
    const userId = req.getPathParam("id");
    const postId = req.getPathParam("post_id");
    
    const rows = await query(
      'SELECT * FROM posts WHERE user_id = $1 AND id = $2',
      [userId, postId]
    );
    
    return HttpResponse.json(rows);
  })
  ```
</CodeGroup>

## Query Parameters

Access URL query string parameters:

<CodeGroup>
  ```rust Rust theme={null}
  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))
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/search", async (req) => {
    const q = req.getQueryParam("q") || "";
    const limit = parseInt(req.getQueryParam("limit") || "10");
    const offset = parseInt(req.getQueryParam("offset") || "0");
    
    const rows = await query(
      'SELECT * FROM items WHERE name LIKE $1 LIMIT $2 OFFSET $3',
      [`%${q}%`, limit, offset]
    );
    
    return HttpResponse.json(rows);
  })
  ```
</CodeGroup>

## Request Body

Process request body data:

<CodeGroup>
  ```rust Rust theme={null}
  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,
      }))
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.post("/users", async (req) => {
    const user = req.json();
    
    const result = await execute(
      'INSERT INTO users (name, email) VALUES ($1, $2)',
      [user.name, user.email]
    );
    
    return HttpResponse.json({
      id: result,
      name: user.name,
      email: user.email,
    });
  })
  ```
</CodeGroup>

## Headers

Access and set HTTP headers:

<CodeGroup>
  ```rust Rust theme={null}
  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)
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/protected", (req) => {
    const authHeader = req.getHeader("authorization");
    
    if (!authHeader || !authHeader.startsWith("Bearer ")) {
      return HttpResponse.builder()
        .status(401)
        .body("Unauthorized");
    }
    
    return HttpResponse.builder()
      .status(200)
      .header("X-Request-Id", "123")
      .json({ status: "ok" });
  })
  ```
</CodeGroup>

## Response Types

### JSON Response

<CodeGroup>
  ```rust Rust theme={null}
  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()],
      })
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/data", (req) => {
    return HttpResponse.json({
      status: "success",
      data: ["item1", "item2"],
    });
  })
  ```
</CodeGroup>

### HTML Response

<CodeGroup>
  ```rust Rust theme={null}
  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())
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/page", (req) => {
    return HttpResponse.html(`
      <!DOCTYPE html>
      <html>
      <head><title>My Page</title></head>
      <body>
        <h1>Welcome</h1>
        <p>Generated by WASM</p>
      </body>
      </html>
    `);
  })
  ```
</CodeGroup>

### Redirect Response

<CodeGroup>
  ```rust Rust theme={null}
  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")
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/old-path", (req) => {
    return HttpResponse.redirect("/new-path");
  })
  ```
</CodeGroup>

### Binary Response

<CodeGroup>
  ```rust Rust theme={null}
  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()
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/download", (req) => {
    const data = new Uint8Array([/* binary data */]);
    
    return HttpResponse.builder()
      .status(200)
      .header("Content-Type", "application/octet-stream")
      .body(data);
  })
  ```
</CodeGroup>

## Database Integration

Endpoints can query and modify the database:

```rust theme={null}
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:

<CodeGroup>
  ```rust Rust theme={null}
  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())
  }
  ```

  ```typescript TypeScript theme={null}
  import { fetch } from "trailbase-wasm/fetch";

  HttpHandler.get("/proxy", async (req) => {
    const q = req.getQueryParam("q") || "rust";
    
    const response = await fetch(
      `https://api.github.com/search/repositories?q=${q}`
    );
    
    return response;
  })
  ```
</CodeGroup>

## Error Handling

<CodeGroup>
  ```rust Rust theme={null}
  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)))
  }
  ```

  ```typescript TypeScript theme={null}
  HttpHandler.get("/items/{id}", async (req) => {
    const id = req.getPathParam("id");
    
    if (!id) {
      return HttpResponse.builder()
        .status(400)
        .json({ error: "Missing ID" });
    }
    
    try {
      const rows = await query(
        'SELECT * FROM items WHERE id = $1',
        [parseInt(id)]
      );
      
      if (rows.length === 0) {
        return HttpResponse.builder()
          .status(404)
          .json({ error: "Item not found" });
      }
      
      return HttpResponse.json(rows[0]);
    } catch (err) {
      return HttpResponse.builder()
        .status(500)
        .json({ error: err.message });
    }
  })
  ```
</CodeGroup>

## Authentication

Access authenticated user information:

```rust theme={null}
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

```rust theme={null}
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

<CardGroup cols={2}>
  <Card title="Server-Side Rendering" href="/advanced/server-side-rendering" icon="server">
    Render dynamic HTML pages
  </Card>

  <Card title="Jobs Scheduler" href="/advanced/jobs-scheduler" icon="clock">
    Schedule background tasks
  </Card>

  <Card title="WASM Components" href="/advanced/wasm-components" icon="cube">
    Learn component development
  </Card>

  <Card title="Vector Search" href="/advanced/vector-search" icon="magnifying-glass">
    Implement semantic search
  </Card>
</CardGroup>
