API Gateway
This example builds a production-ready API server with comprehensive configuration: environment variables, CORS, rate limiting, compression, caching, sessions, file uploads, TLS, middleware composition, and health checks. It serves as a template for deploying Tova in production.
The Full Application
shared {
type User {
id: Int
name: String
email: String
role: String
}
type ApiMeta {
timestamp: String
request_id: String
}
type ApiResponse<T> {
data: T
meta: ApiMeta
}
type ApiError {
code: Int
message: String
details: Option<String>
}
}
server {
// --- Environment Variables ---
env PORT: Int = 3000
env HOST: String = "0.0.0.0"
env DATABASE_URL: String
env JWT_SECRET: String
env ALLOWED_ORIGINS: String = "http://localhost:5173"
env MAX_UPLOAD_MB: Int = 10
env RATE_LIMIT_RPM: Int = 100
env LOG_LEVEL: String = "info"
env TLS_CERT: Option<String> = None
env TLS_KEY: Option<String> = None
// --- CORS ---
cors {
origins: env("ALLOWED_ORIGINS") |> split(","),
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
headers: ["Content-Type", "Authorization", "X-Request-ID"],
credentials: true,
max_age: 86400
}
// --- Rate Limiting ---
rate_limit {
requests: RATE_LIMIT_RPM,
window: 1.minute,
key: fn(req) req.ip,
on_limit: fn(req, res) {
res.status(429)
res.json({ error: "Rate limit exceeded. Try again later." })
}
}
// --- Compression ---
compression {
enabled: true,
threshold: 1024,
encodings: ["gzip", "deflate"]
}
// --- Caching ---
cache {
default: "public, max-age=300, stale-while-revalidate=60"
}
// --- Static Files ---
static "/" => "public" fallback "index.html"
// --- Sessions ---
session {
secret: JWT_SECRET,
cookie: "session",
max_age: 7.days,
secure: true,
http_only: true,
same_site: "strict"
}
// --- File Uploads ---
upload {
max_size: MAX_UPLOAD_MB.megabytes,
allowed_types: ["image/png", "image/jpeg", "image/webp", "application/pdf"],
destination: "uploads/"
}
// --- TLS ---
tls {
cert: TLS_CERT,
key: TLS_KEY
}
// --- Max Body Size ---
max_body 5.megabytes
// --- Database ---
db {
adapter: "postgres"
url: DATABASE_URL
pool: 20
}
model User {
name: String
email: String
role: String
password_hash: String
}
// --- Middleware ---
middleware fn request_id(req, res) {
id = req.headers["x-request-id"] |> unwrapOr(uuid())
req.id = id
res.setHeader("X-Request-ID", id)
}
middleware fn logger(req, res) {
start = Date.now()
print("[{LOG_LEVEL}] {req.method} {req.path} - started (id: {req.id})")
res.on_finish(fn() {
duration = Date.now() - start
print("[{LOG_LEVEL}] {req.method} {req.path} - {res.status} ({duration}ms)")
})
}
middleware fn auth(req, res) {
token = req.headers["authorization"]
|> map(fn(h) h |> replace("Bearer ", ""))
match token {
None => {
res.status(401)
res.json({ error: "Authorization header required" })
}
Some(t) => {
match jwt.verify(t, JWT_SECRET) {
Ok(payload) => { req.user = payload }
Err(_) => {
res.status(401)
res.json({ error: "Invalid or expired token" })
}
}
}
}
}
middleware fn require_role(role: String) {
fn(req, res) {
guard req.user.role == role else {
res.status(403)
res.json({ error: "Requires role: {role}" })
return
}
}
}
// --- Error Handler ---
on_error fn(err, req, res) {
print("[ERROR] {req.method} {req.path} - {err.message} (id: {req.id})")
code = match err.status {
Some(status) => status
None => 500
}
res.status(code)
api_err = ApiError {
code: code,
message: match code {
400 => "Bad request"
401 => "Unauthorized"
403 => "Forbidden"
404 => "Not found"
429 => "Rate limit exceeded"
_ => "Internal server error"
},
details: match LOG_LEVEL {
"debug" => Some(err.message)
_ => None
}
}
res.json(api_err)
}
// --- Health Check ---
fn health() {
{
status: "ok",
uptime: Process.uptime(),
version: env("APP_VERSION") |> unwrapOr("dev"),
timestamp: Date.now()
}
}
// --- API Routes ---
fn list_users(req) -> [User] {
page = req.query.page |> unwrapOr(1)
per_page = req.query.per_page |> unwrapOr(20)
User.all() |> paginate(page, per_page)
}
fn get_user(req, id: Int) -> Result<User, ApiError> {
not_found = ApiError { code: 404, message: "User not found", details: None }
User.find(id)
|> ok_or(not_found)
}
fn create_user(req) -> Result<User, ApiError> {
guard req.body.name |> len() > 0 else {
name_err = ApiError { code: 400, message: "Name required", details: None }
return Err(name_err)
}
guard req.body.email |> contains("@") else {
email_err = ApiError { code: 400, message: "Valid email required", details: None }
return Err(email_err)
}
hash = Bun.password.hashSync(req.body.password, { algorithm: "bcrypt" })
User.create({
name: req.body.name,
email: req.body.email,
role: "user",
password_hash: hash
}) |> Ok()
}
fn upload_avatar(req) {
file = req.file
{
filename: file.name,
size: file.size,
url: "/uploads/{file.name}"
}
}
// --- Route Registration ---
route GET "/health" => health
route GET "/api/users" => list_users with auth
route GET "/api/users/:id" => get_user with auth
route POST "/api/users" => create_user with auth, require_role("admin")
route POST "/api/users/avatar" => upload_avatar with auth
}Running It
# Development
DATABASE_URL="postgres://localhost/myapp" JWT_SECRET="dev-secret" tova dev gateway.tova
# Production
tova build gateway.tova
DATABASE_URL="..." JWT_SECRET="..." TLS_CERT="/path/cert.pem" TLS_KEY="/path/key.pem" bun run dist/server.jsWhat This Demonstrates
Environment Variables with Types
env PORT: Int = 3000
env DATABASE_URL: String // Required — no default
env TLS_CERT: Option<String> = None // OptionalTyped env declarations validate environment variables at startup. Missing required variables without defaults cause a startup error. Option types allow genuinely optional configuration.
CORS Configuration
cors {
origins: env("ALLOWED_ORIGINS") |> split(","),
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
credentials: true,
max_age: 86400
}Origins can be dynamic (from environment) or static. max_age controls preflight cache duration.
Rate Limiting
rate_limit {
requests: 100,
window: 1.minute,
key: fn(req) req.ip,
on_limit: fn(req, res) { res.status(429); ... }
}The key function determines how requests are grouped (by IP, by user, by API key). The on_limit handler customizes the response.
Middleware Composition
Middleware is applied with with on route declarations:
route GET "/api/users" => list_users with auth
route POST "/api/users" => create_user with auth, require_role("admin")Multiple middleware run in order: request_id → logger → auth → require_role → handler. Global middleware (declared without routes) runs on every request. Per-route middleware is added with with.
Parameterized Middleware
middleware fn require_role(role: String) {
fn(req, res) {
guard req.user.role == role else { ... }
}
}require_role("admin") returns a middleware function. This pattern creates configurable middleware.
Global Error Handler
on_error fn(err, req, res) {
// Log the error with request ID for tracing
// Return structured error response
// Hide details in production
}The on_error handler catches all unhandled errors and returns consistent ApiError responses. The LOG_LEVEL env var controls whether error details are exposed.
Health Check
route GET "/health" => health // No middleware — always accessibleThe health endpoint returns uptime, version, and timestamp. No auth middleware so load balancers and orchestrators can probe it.
Sessions and File Uploads
sessions {
secret: JWT_SECRET,
max_age: 7.days,
secure: true, http_only: true, same_site: "strict"
}
uploads {
max_size: 10.megabytes,
allowed_types: ["image/png", "image/jpeg", "application/pdf"],
destination: "uploads/"
}Session cookies are configured for security. File uploads are restricted by size and MIME type.
TLS
tls {
cert: TLS_CERT,
key: TLS_KEY
}When TLS_CERT and TLS_KEY are provided, the server runs over HTTPS. When they're None, it runs over HTTP (suitable for development or when behind a reverse proxy).
Key Patterns
Environment-driven configuration. All deployment-specific values come from env declarations with sensible defaults for development.
Layered middleware. Global middleware (request_id, logger) applies everywhere. Auth middleware applies per-route. Parameterized middleware (require_role) adds fine-grained access control.
Structured errors. A single ApiError type and on_error handler ensure consistent error responses across all endpoints.
Production security. CORS, rate limiting, TLS, secure sessions, and body size limits are all configured declaratively.