Skip to content

Advanced

This page covers advanced server features including background jobs, scheduled tasks, lifecycle hooks, service discovery, event-driven architecture, distributed tracing, and auto-generated API documentation.

Background Jobs

Background jobs let you offload work that does not need to complete before responding to a request. Declare a background function and dispatch it with spawn_job:

tova
server {
  background fn send_email(to, subject, body) {
    // Runs in the background, does not block the request
    mail.send(to, subject, body)
  }

  fn register_user(req) {
    let { name, email } = req.body
    user = UserModel.create({ name: name, email: email })
    spawn_job("send_email", email, "Welcome!", "Hello {name}, welcome aboard!")
    respond(201, user)
  }

  route POST "/api/register" => register_user
}

The spawn_job call returns immediately. The background function runs asynchronously without blocking the HTTP response.

spawn_job

tova
spawn_job("function_name", arg1, arg2, ...)
ParameterDescription
First argumentThe name of the background function (as a string)
Remaining argumentsArguments passed to the background function

Scheduled Tasks

Run functions on a recurring schedule using cron expressions:

tova
server {
  schedule "*/5 * * * *" fn cleanup() {
    db.run("DELETE FROM sessions WHERE expires_at < ?", Date.now())
  }

  schedule "0 0 * * *" fn daily_report() {
    // Runs at midnight every day
    report = generate_report()
    spawn_job("send_email", "admin@example.com", "Daily Report", report)
  }

  schedule "0 */6 * * *" fn sync_data() {
    // Runs every 6 hours
    fetch_external_data()
  }
}

Cron Expression Format

* * * * *
| | | | |
| | | | +-- day of week (0-7, where 0 and 7 are Sunday)
| | | +---- month (1-12)
| | +------ day of month (1-31)
| +-------- hour (0-23)
+---------- minute (0-59)
ExpressionMeaning
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 0 * * *Daily at midnight
0 0 * * 1Every Monday at midnight
0 9,17 * * 1-59 AM and 5 PM on weekdays

Lifecycle Hooks

Run code when the server starts or stops:

tova
server {
  on_start fn() {
    print("Server started")
    db.migrate()
    cache.warm()
  }

  on_stop fn() {
    print("Server shutting down")
    db.close()
    cache.flush()
  }
}

on_start

Runs after the server binds to a port and is ready to accept requests. Use it for initialization tasks like running migrations, warming caches, or logging startup information.

on_stop

Runs when the server receives a shutdown signal (e.g., SIGTERM, SIGINT). Use it for cleanup tasks like closing database connections, flushing buffers, or deregistering from service discovery.

Error Handling

Define a global error handler that catches unhandled errors in route handlers:

tova
server {
  on_error fn(err, req) {
    print("Error on {req.method} {req.url}: {err}")
    respond(500, {
      error: "Internal server error",
      message: err.message
    })
  }
}

The error handler receives the error object and the original request. Without a custom error handler, unhandled errors return a generic 500 response.

Service Discovery

In multi-server architectures (using named server blocks), discover lets one server call functions on another. Tova includes a built-in circuit breaker to protect against cascading failures:

tova
server "api" {
  discover "events" at "http://localhost:3002"
  discover "auth" at "http://localhost:3003" with {
    threshold: 5              // open circuit after 5 consecutive failures
    timeout: 3000             // request timeout in milliseconds
    reset_timeout: 30000      // try again after 30 seconds
  }
}

Circuit Breaker Options

OptionTypeDescription
thresholdIntNumber of consecutive failures before the circuit opens
timeoutIntRequest timeout in milliseconds
reset_timeoutIntTime in milliseconds before the circuit transitions from open to half-open

Once a service is discovered, you can call its functions as if they were local:

tova
// Calls the "events" service's create_event function via RPC
events.create_event({ type: "user_signup", user_id: user.id })

When the circuit is open, calls fail immediately without attempting the network request, preventing a failing downstream service from slowing down your server.

Event Bus (Pub/Sub)

Named server blocks can communicate via an event bus. Use subscribe to listen for events and publish to emit them:

tova
server "api" {
  subscribe "user.created" fn(data) {
    print("New user created: {data.name}")
    spawn_job("send_welcome_email", data.email)
  }

  subscribe "order.completed" fn(data) {
    print("Order {data.id} completed")
  }

  fn create_user(req) {
    user = UserModel.create(req.body)
    publish("user.created", user)    // notifies all subscribers + peer servers
    respond(201, user)
  }

  route POST "/api/users" with auth => create_user
}

Events are delivered to all subscribers in the current server and to peer servers in a multi-server setup. This enables loose coupling between services.

Event Bus Functions

FunctionDescription
subscribe(event, handler)Register a handler for a named event
publish(event, data)Emit an event with associated data

Distributed Tracing

Tova automatically generates and propagates request IDs across server boundaries. This makes it possible to trace a request through multiple services.

Request ID Functions

tova
request_id = __getRequestId()     // get the current request's trace ID
locals = __getLocals()            // get request-scoped storage (AsyncLocalStorage)

Automatic Propagation

When one server calls another via RPC or service discovery, the X-Request-Id header is automatically propagated. This creates a trace that spans multiple services:

Client -> Server A (X-Request-Id: abc-123)
           |
           +-> Server B (X-Request-Id: abc-123)
           |
           +-> Server C (X-Request-Id: abc-123)

Use the request ID in logging to correlate log entries across services:

tova
middleware fn trace_logger(req, next) {
  rid = __getRequestId()
  print("[{rid}] {req.method} {req.url}")
  result = next(req)
  print("[{rid}] completed")
  result
}

OpenAPI Auto-Generation

Tova automatically generates OpenAPI 3.0 documentation from your routes and types. Two endpoints are available by default:

EndpointDescription
GET /openapi.jsonOpenAPI 3.0 specification in JSON format
GET /docsInteractive Swagger UI for exploring and testing your API

Route parameters, request bodies, and response types are derived from function signatures and shared type declarations. No manual documentation is needed.

Example

Given this server:

tova
shared {
  type User {
    id: Int
    name: String
    email: String
  }
}

server {
  model User

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

  fn create_user(req) -> User {
    UserModel.create(req.body)
  }

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

The generated OpenAPI spec includes the /api/users endpoints with the User schema, parameter types, and authentication requirements.

Cache Control Helpers

Fine-tune HTTP caching on individual responses:

tova
cache_control(response, 3600, { private: true })     // Cache-Control: private, max-age=3600
etag(response, "hash123")                             // ETag: "hash123"

cache_control

tova
cache_control(response, max_age)
cache_control(response, max_age, options)
ParameterTypeDescription
responseResponseThe response to set headers on
max_ageIntCache lifetime in seconds
optionsObject?Additional directives like private, no_cache, no_store

etag

tova
etag(response, hash)

Sets the ETag header on a response. Clients can use this for conditional requests with If-None-Match, enabling 304 Not Modified responses.

Complete Advanced Example

Here is a server combining several advanced features:

tova
server "api" {
  env PORT: Int = 3000
  db { path: "./data.db" }
  auth { type: "jwt", secret: "secret" }

  // Service discovery
  discover "notifications" at "http://localhost:3002"

  // Lifecycle
  on_start fn() {
    db.migrate()
    print("API server started on port {PORT}")
  }

  on_stop fn() {
    db.close()
    print("API server stopped")
  }

  // Error handling
  on_error fn(err, req) {
    rid = __getRequestId()
    print("[{rid}] Error: {err.message}")
    respond(500, { error: "Internal server error", request_id: rid })
  }

  // Background jobs
  background fn send_welcome(email, name) {
    mail.send(email, "Welcome!", "Hello {name}!")
  }

  // Scheduled tasks
  schedule "0 * * * *" fn hourly_cleanup() {
    db.run("DELETE FROM sessions WHERE expires_at < ?", Date.now())
  }

  // Event bus
  subscribe "user.created" fn(user) {
    notifications.notify({ type: "new_user", data: user })
  }

  // Routes
  model User

  fn create_user(req) {
    user = UserModel.create(req.body)
    spawn_job("send_welcome", user.email, user.name)
    publish("user.created", user)
    respond(201, user)
  }

  route POST "/api/users" with auth => create_user
}

Practical Tips

Use background jobs for slow operations. Email sending, image processing, and external API calls should not block the HTTP response. Use background fn and spawn_job to offload them.

Set up lifecycle hooks for clean startup and shutdown. Run migrations and warm caches in on_start. Close connections and flush state in on_stop. This ensures your server starts in a known-good state and shuts down gracefully.

Configure circuit breakers for service calls. When using discover, always set a reasonable timeout and threshold. Without circuit breakers, a single slow or failing service can bring down your entire system.

Use the event bus for loose coupling. Instead of having services call each other directly for every interaction, publish events and let interested services subscribe. This reduces coupling and makes your architecture more resilient.

Check /docs during development. The auto-generated Swagger UI is a quick way to explore and test your API without writing a separate client.

Released under the MIT License.