Serv Language Reference

Program Structure

A Serv program consists of top-level declarations and statements:

server "8080"                    // Infrastructure
database "sqlite://app.db"       // Database connection
cache "redis://localhost:6379"   // Cache connection
broker "nats://localhost:4222"   // Message broker

// Routes, functions, scheduled tasks, etc.

Unified Application Block (app)

An app block acts as a namespace to group related servers, databases, and APIs within a single logical service boundary:

app GatewayService {
    server "8080"
    database "sqlite://app.db"

    export route "GET" "/health" (req) {
        return { "status": "UP" }
    }
}

Variables

let name = "Alice"               // Type inferred
let age: int = 30                // Explicit type
let { x, y } = point            // Destructuring
let val, err = riskyFunction()   // Multi-return

Types

TypeExample
int42
float3.14
string"hello"
booltrue, false
nilnil
[]T[1, 2, 3]
map{ "key": "value" }
T?Optional (nullable) type
T | UUnion type

Type Aliases

type UserID = int
type Email = string

Optional Types (Null Safety)

Types suffixed with ? allow nil values. Without ?, assigning nil is a compile error.

let name: string = "Alice"     // Cannot be nil
let email: string? = nil       // OK — optional type

fn findUser(id: int) -> User? {
    let row = db.query("SELECT * FROM users WHERE id = ?", id)
    if row == nil { return nil }
    return User { name: row.name }
}

Compile error example:

let x: int = nil   // error: cannot assign nil to non-optional type 'int' (use 'int?' to allow nil)

Union Types

Union types allow a value to be one of several types:

fn divide(a: int, b: int) -> int | error {
    if b == 0 {
        return "division by zero"
    }
    return a / b
}

fn process(input: string | int) {
    log.info(input)
}

Functions

// Basic function
fn greet(name) {
    return f"Hello, {name}!"
}

// Typed parameters and return
fn add(a: int, b: int) -> int {
    return a + b
}

// Generic function
fn identity[T](value: T) -> T {
    return value
}

// Generic with constraints
fn max[T: Ordered](a: T, b: T) -> T {
    if a > b { return a }
    return b
}

// Arrow functions (closures)
let double = x => x * 2
let add = fn(a, b) { return a + b }

Generic Constraints

ConstraintSupports
Comparable==, !=
Ordered<, >, <=, >=
Numeric+, -, *, /
IntegerInteger arithmetic
FloatFloating point

Control Flow

If/Else

if condition {
    // ...
} else if other {
    // ...
} else {
    // ...
}

For Loops

// Range-based
for item in items {
    log.info(item)
}

// Key-value iteration (maps)
for key, value in config {
    log.info(f"{key} = {value}")
}

// Condition-based
for count < 10 {
    count += 1
}

Break & Continue

for item in items {
    if item == nil { continue }
    if item == "stop" { break }
    log.info(item)
}

Match (Pattern Matching)

match status {
    "active" => { log.info("Active") }
    "inactive" => { log.info("Inactive") }
    _ => { log.info("Unknown") }
}

Structs

struct User {
    name: string,
    email: string,
    age: int
}

// Methods
fn User.greet() -> string {
    return f"Hi, I'm {self.name}"
}

// Instantiation
let user = User { name: "Alice", email: "a@test.com", age: 30 }
log.info(user.greet())

Enums

// Simple (string values)
enum Color { Red, Green, Blue }

// With explicit values
enum HttpStatus {
    OK = 200,
    NotFound = 404,
    ServerError = 500
}

Interfaces

interface Serializable {
    fn serialize() -> string
    fn deserialize(data: string)
}

HTTP Routes

route "GET" "/users" (req) {
    return { "users": [] }
}

route "POST" "/users" (req) {
    let body = req.body
    return { "created": true }
}

// With rate limiting
route "GET" "/api/data" (req) limit 100/minute {
    return { "data": "limited" }
}

// With middleware
route "GET" "/protected" (req) use [auth, logging] {
    return { "secret": "data" }
}

Request Object

FieldTypeDescription
req.bodystringRequest body (JSON string)
req.methodstringHTTP method
req.pathstringURL path
req.paramsmapURL params + headers

WebSockets

ws "/chat" (conn) {
    for true {
        let msg = conn.receive()
        if msg == nil { break }
        conn.send(f"Echo: {msg}")
    }
}

Scheduled Tasks

// Fixed interval
every 5s {
    log.info("Tick")
}

// Cron expression
cron "0 0 * * *" {
    log.info("Midnight job")
}

Pub/Sub Messaging

// Subscribe to a topic
subscribe "orders.new" (msg) {
    log.info("New order: ", msg)
}

// Publish a message
publish "notifications" "Order confirmed"

Concurrency

// Fire and forget
spawn processOrder(order)

// With worker pool limit
spawn(5) heavyTask(data)

// Async/await
let result = await fetchData()
let all = await all([task1(), task2(), task3()])

Error Handling

// Try/catch (traditional)
try {
    let result = http.get("http://api.example.com/data")
    log.info(result.body)
} catch (err) {
    log.error("Failed: ", err)
}

// Multi-return error handling
let data, err = riskyCall()
if err != nil {
    log.error(err)
}

// ? operator — early return on error (recommended)
fn loadUser(id: int) -> User? {
    let row = db.query("SELECT * FROM users WHERE id = ?", id)?
    let parsed = json.parse(row)?
    return User { name: parsed.name }
}

The ? operator calls the expression and:

  • If it returns nil or an error, returns nil from the enclosing function
  • If it succeeds, unwraps the value and continues

Middleware

middleware auth(req) {
    let token = req.params.authorization
    if token == nil {
        return { "error": "Unauthorized", "status": 401 }
    }
}

route "GET" "/protected" (req) use [auth] {
    return { "data": "secret" }
}

Optional Chaining

let city = user?.address?.city    // nil if any part is nil

Spread Operator

let defaults = { "timeout": 30, "retries": 3 }
let config = { ...defaults, "timeout": 60 }

Operators

Arithmetic

OperatorDescriptionExample
+Addition / concatenationa + b
-Subtractiona - b
*Multiplicationa * b
/Divisiona / b
%Modulo (remainder)a % b

Compound Assignment

let count = 0
count += 1       // count = count + 1
count -= 1       // count = count - 1
count *= 2       // count = count * 2
count /= 2       // count = count / 2
count %= 3       // count = count % 3

Bitwise Operators

OperatorDescriptionExample
&Bitwise ANDa & b
|Bitwise ORa | b
^Bitwise XORa ^ b
<<Left shifta << 2
>>Right shifta >> 1

Comparison

OperatorDescription
==Equal
!=Not equal
<Less than
>Greater than
<=Less than or equal
>=Greater than or equal

Logical

OperatorDescription
andLogical AND
orLogical OR
!Logical NOT

Slice Expressions

let items = [1, 2, 3, 4, 5]
let first3 = items[0:3]     // [1, 2, 3]
let rest = items[2:]         // [3, 4, 5]
let head = items[:2]         // [1, 2]

let text = "hello world"
let sub = text[0:5]          // "hello"

Imports & Modules

// Import a local .pnr module (relative path)
import "models/user.pnr"
import { User, Role } from "models/user.pnr"

// Import from stdlib (no relative path needed)
import { ok, notFound } from "stdlib/response"
import { requireAuth } from "stdlib/auth"
import { hashPassword } from "stdlib/crypto"

// Import a Go package
import uuid from "github.com/google/uuid"
let id = uuid.New()

// .pnr extension is optional for stdlib imports
import { maskEmail } from "stdlib/mask.pnr"   // also works

Import resolution order:

  1. stdlib/X — resolved from project root's stdlib/ directory
  2. ./path or ../path — resolved relative to the importing file
  3. Bare path — resolved relative to the importing file

External Function Bindings

// Go package
extern fn generateID() from "go:github.com/google/uuid:NewString"

// Python script
extern fn analyze(data) from "python:./scripts/analyzer.py:analyze"

Testing

test "math works" {
    let result = add(2, 3)
    assert result == 5          // "got X, want 5" on failure
}

test "comparisons" {
    assert 10 > 5               // "10 is not > 5" on failure
    assert "hello" != "world"   // "expected value to not equal world" on failure
}

test "string methods" {
    assert "hello".toUpper() == "HELLO"
    assert "  hi  ".trim() == "hi"
}

Assertion messages:

  • assert x == 5assertion failed: got 3, want 5
  • assert x != 0assertion failed: expected value to not equal 0
  • assert x > 10assertion failed: 5 is not > 10
  • assert validassertion failed: expected truthy value, got false

Config Validation

validate {
    required "db.host",
    required "db.port",
    optional "log.level"
}

Request Validation

let errors = validate(req.body, {
    "email": "required,email",
    "name": "required,string",
    "age": "int"
})

Declarative Schema Migrations (table)

Declare your database schema natively in .pnr files. The compiler generates the SQL automatically; serv migrate applies it to the live database.

table users {
    id        int      @primary @autoincrement
    name      string   @required
    email     string   @unique
    role      string   @default(user)
    createdAt datetime @default(now)
}

table posts {
    id        int      @primary @autoincrement
    userId    int      @required
    title     string   @required
    body      string
    published bool     @default(0)
    createdAt datetime @default(now)
}

Column Annotations

AnnotationSQL equivalentNotes
@primaryPRIMARY KEYMark as primary key
@autoincrementAUTOINCREMENTAuto-increment integer (SQLite)
@requiredNOT NULLField cannot be null
@uniqueUNIQUEEnforce unique constraint
@default(value)DEFAULT valueSet default; use now for CURRENT_TIMESTAMP

Serv → SQL Type Mapping

Serv typeSQL type
intINTEGER
floatREAL
boolINTEGER (0/1)
stringTEXT
datetimeDATETIME

serv migrate workflow

# Apply all table declarations to the database (default: sqlite://serv.db)
serv migrate

# Target a specific file or directory
serv migrate ./schemas/

# Override the database connection
serv migrate --db sqlite://production.db
serv migrate --db postgres://user:pass@localhost/mydb

serv migrate will:

  • Create tables that don't exist yet (CREATE TABLE IF NOT EXISTS)
  • Add missing columns to existing tables (ALTER TABLE ADD COLUMN)
  • Skip tables/columns that are already up to date

Note: Column renames and type changes require a manual migration block (see below).

Raw SQL migrations (legacy / advanced)

For custom logic, constraints, or renaming operations use the migration block:

migration "add_users_index" {
    db.query("CREATE INDEX idx_users_email ON users (email)")
}

migration "rename_status_column" {
    db.query("ALTER TABLE orders RENAME COLUMN status TO order_status")
}

Raw migrations are applied in declaration order and tracked in schema_migrations.

MCP Tools

tool "calculator" "Performs math operations" (args) {
    let result = args.a + args.b
    return { "result": result }
}

AI Agents (agent)

Declare autonomous AI agents with system prompts, model routing, and tool bindings:

agent SupportBot {
    system "You are a helpful customer support assistant."
    model  "openai://gpt-4o"
    tools  ["lookup_order", "create_ticket"]
}

tool "lookup_order" "Look up an order by ID" (args) {
    let row = db.query("SELECT * FROM orders WHERE id = ?", args.order_id)
    return row
}

Supported model URI schemes:

  • openai://gpt-4o — OpenAI GPT-4
  • anthropic://claude-3-5-sonnet — Anthropic Claude
  • google://gemini-2.0-flash — Google Gemini
  • local://ollama/llama3 — Local Ollama model

Agent configuration keys:

KeyDescription
systemSystem prompt / instruction
modelModel URI
toolsList of tool block names available to the agent

Foreign Function Interface (FFI)

Import and call external Go packages or receiver methods directly:

# Import Go packages
extern fn newUUID() -> string from "go:github.com/google/uuid:NewString"

# Bind receiver methods
extern fn decimalToString(d) from "go:github.com/shopspring/decimal:Decimal.String"

Stream DSL WASM Transforms (transform)

Declare inline WASM stream transforms in under 5 lines:

transform "orders.raw" (msg) {
    let clean = msg
    return clean
}

Logic Configuration Policy Engine (policy)

Define dynamic routing and authorization policies evaluated at proxy speed:

policy rate_limit_policy (ctx) {
    let path = ctx["path"]
    if path == "/api/admin" {
        return false
    }
    return true
}