Skip to content

Server Block

The server block defines everything that runs on the server. It compiles to a standalone Bun script that starts an HTTP server using Bun.serve, registers route handlers, sets up database connections, and exposes RPC endpoints for any functions the client needs to call.

Purpose

The server block is where your application's backend logic lives:

  • HTTP routes -- handle REST API requests
  • Database access -- connect to SQLite/databases, define models
  • Server functions -- business logic callable from the client via RPC
  • Authentication and middleware -- protect routes and validate requests
  • WebSocket and SSE -- real-time communication
  • Background jobs -- scheduled tasks and workers

Basic Structure

tova
server {
  db { path: "./data.db" }
  model User

  fn get_users() -> [User] {
    UserModel.all()
  }

  fn create_user(name: String, email: String) -> User {
    UserModel.create({ name, email })
  }

  route GET "/api/users" => get_users
  route POST "/api/users" => create_user
}

This compiles to a Bun HTTP server that:

  • Opens a SQLite database at ./data.db
  • Registers a User model
  • Creates two route handlers for GET /api/users and POST /api/users
  • Automatically generates POST /rpc/get_users and POST /rpc/create_user endpoints for client RPC calls

Server Functions and RPC

Every function defined in a server block automatically gets an RPC endpoint. This is the primary way the client communicates with the server:

tova
server {
  fn get_users() -> [User] {
    UserModel.all()
  }

  fn search_users(query: String, limit: Int) -> [User] {
    UserModel.where("name LIKE ?", "%{query}%").limit(limit)
  }

  fn create_user(name: String, email: String) -> User {
    guard validate_email(email) else {
      return error("Invalid email address")
    }
    UserModel.create({ name, email })
  }
}

The compiler generates:

  • POST /rpc/get_users -- calls get_users(), returns { result: [...] }
  • POST /rpc/search_users -- extracts query and limit from the request body, calls search_users(query, limit)
  • POST /rpc/create_user -- extracts name and email, runs validation, calls create_user(name, email)

From the client, these are called as server.get_users(), server.search_users("alice", 10), etc. See RPC Bridge for the full details.

Type-Based Validation

When you annotate server function parameters with types, the compiler generates automatic validation code in the RPC endpoint:

tova
server {
  fn create_user(name: String, age: Int, active: Bool) -> User {
    UserModel.create({ name, age, active })
  }
}

The generated endpoint checks:

  • name is required and must be a string
  • age is required and must be an integer
  • active is required and must be a boolean

If validation fails, the endpoint returns a 400 response with details before the function body executes.

Routes

Routes map HTTP methods and paths to handler functions:

tova
server {
  route GET "/api/users" => list_users
  route POST "/api/users" => create_user
  route GET "/api/users/:id" => get_user
  route PUT "/api/users/:id" => update_user
  route DELETE "/api/users/:id" => delete_user
}

Supported HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.

Path Parameters

Path segments prefixed with : are extracted and passed as arguments:

tova
server {
  route GET "/api/users/:id" => get_user

  fn get_user(id: String) {
    UserModel.find(id)
  }
}

With type annotations, parameters are automatically parsed and validated:

tova
server {
  fn get_user(id: Int) {
    UserModel.find(id)  // id is auto-parsed as Int
  }
}

Route Guards

The with keyword attaches middleware or guards to specific routes:

tova
server {
  route GET "/admin/users" with auth => list_users
  route DELETE "/api/users/:id" with auth, role("admin") => delete_user
}

Database

The db declaration sets up a database connection:

tova
server {
  db { path: "./data.db" }

  model User
  model Post
}

Models provide an ORM-like interface for querying:

tova
server {
  fn get_all_users() -> [User] {
    UserModel.all()
  }

  fn find_user(id: Int) -> User {
    UserModel.find(id)
  }

  fn search(query: String) -> [User] {
    UserModel.where("name LIKE ?", "%{query}%")
  }
}

Middleware

Middleware functions run before route handlers:

tova
server {
  middleware fn log_request(req) {
    print("{req.method} {req.url}")
  }

  middleware fn cors(req) {
    // CORS headers added automatically
  }
}

Authentication

Built-in auth support with JWT:

tova
server {
  auth {
    secret: "my-secret-key"
    algorithm: "HS256"
  }

  fn protected_action() {
    // Only accessible with valid token
  }

  route GET "/api/me" with auth => get_profile
}

WebSocket

Real-time bidirectional communication:

tova
server {
  ws {
    on_open fn(socket) {
      print("Client connected")
    }

    on_message fn(socket, message) {
      socket.send("Echo: {message}")
    }

    on_close fn(socket) {
      print("Client disconnected")
    }
  }
}

Server-Sent Events

One-way streaming from server to client:

tova
server {
  sse "/events" fn(send, close) {
    send("connected", { status: "ok" })

    // Send periodic updates
    every 5.seconds() {
      send("heartbeat", { time: Date.now() })
    }
  }
}

Health Checks

Built-in health endpoint:

tova
server {
  health "/healthz"
}

Generates a GET /healthz endpoint that returns { "status": "ok", "uptime": ... }.

Error Handling

Custom error handlers for unhandled errors:

tova
server {
  on_error fn(error, req) {
    print("Error: {error.message}")
    { status: 500, body: "Internal Server Error" }
  }
}

Configuration

Server configuration with environment variables:

tova
server {
  config {
    port: 3000
    host: "0.0.0.0"
  }
}

The port can be overridden with the PORT environment variable.

Compilation Output

The server block compiles to a standalone Bun script. For app.tova:

.tova-out/
  app.server.js    <-- standalone Bun.serve script

The generated file:

  1. Imports shared types from app.shared.js
  2. Defines all server functions
  3. Registers RPC endpoints (POST /rpc/<name>) for each function
  4. Registers explicit routes (GET /api/users, etc.)
  5. Starts the server with Bun.serve()
  6. Serves the client HTML on the root route (/)

Run it with:

bash
bun run .tova-out/app.server.js

Or use the dev server which handles this automatically:

bash
tova dev

For detailed reference on each server feature, see the Server Reference pages:

Released under the MIT License.