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
| Type | Example |
|---|---|
int | 42 |
float | 3.14 |
string | "hello" |
bool | true, false |
nil | nil |
[]T | [1, 2, 3] |
map | { "key": "value" } |
T? | Optional (nullable) type |
T | U | Union 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
| Constraint | Supports |
|---|---|
Comparable | ==, != |
Ordered | <, >, <=, >= |
Numeric | +, -, *, / |
Integer | Integer arithmetic |
Float | Floating 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
| Field | Type | Description |
|---|---|---|
req.body | string | Request body (JSON string) |
req.method | string | HTTP method |
req.path | string | URL path |
req.params | map | URL 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
nilor an error, returnsnilfrom 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
| Operator | Description | Example |
|---|---|---|
+ | Addition / concatenation | a + b |
- | Subtraction | a - b |
* | Multiplication | a * b |
/ | Division | a / 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
| Operator | Description | Example |
|---|---|---|
& | Bitwise AND | a & b |
| | Bitwise OR | a | b |
^ | Bitwise XOR | a ^ b |
<< | Left shift | a << 2 |
>> | Right shift | a >> 1 |
Comparison
| Operator | Description |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
Logical
| Operator | Description |
|---|---|
and | Logical AND |
or | Logical 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:
stdlib/X— resolved from project root'sstdlib/directory./pathor../path— resolved relative to the importing file- 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 == 5→assertion failed: got 3, want 5assert x != 0→assertion failed: expected value to not equal 0assert x > 10→assertion failed: 5 is not > 10assert valid→assertion 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
| Annotation | SQL equivalent | Notes |
|---|---|---|
@primary | PRIMARY KEY | Mark as primary key |
@autoincrement | AUTOINCREMENT | Auto-increment integer (SQLite) |
@required | NOT NULL | Field cannot be null |
@unique | UNIQUE | Enforce unique constraint |
@default(value) | DEFAULT value | Set default; use now for CURRENT_TIMESTAMP |
Serv → SQL Type Mapping
| Serv type | SQL type |
|---|---|
int | INTEGER |
float | REAL |
bool | INTEGER (0/1) |
string | TEXT |
datetime | DATETIME |
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-4anthropic://claude-3-5-sonnet— Anthropic Claudegoogle://gemini-2.0-flash— Google Geminilocal://ollama/llama3— Local Ollama model
Agent configuration keys:
| Key | Description |
|---|---|
system | System prompt / instruction |
model | Model URI |
tools | List 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
}