Complete Web Application
This chapter presents a complete, production-style web application using PostgreSQL, HTML templating, connection pooling, routing, forms, and database transactions. All techniques from previous chapters come together here.
Features
- Connection Pooling - Efficient database access via
stdlib.pool - HTML Templating - Type-safe templates with
Html(...) - Routing - Path matching with
Server.matchPath - Form Handling - POST data with validation
- Transactions - Atomic multi-statement updates
- Spawn-per-request - Scalable request handling
Prerequisites
This example requires PostgreSQL running at localhost with user/password: postgres/postgres.
Testing the Application
# Start the server
./nostos examples/web_server_complete.nos
# Test endpoints
curl http://localhost:8080/
curl http://localhost:8080/users
curl -X POST -d "name=Alice&email=alice@example.com" http://localhost:8080/users
curl -X POST -d "from=1&to=2&amount=50" http://localhost:8080/transfer
stdlib.pool - Connection Pool
The stdlib.pool module manages a pool of PostgreSQL connections. Instead of connecting
per-request, connections are reused for better performance.
use stdlib.pool.{init, initWithMax, query, execute, transaction, stats}
# Initialize pool at startup (default max: 25 connections)
init("host=localhost user=postgres password=postgres")
# Or initialize with custom pool size for high-load applications
initWithMax("host=localhost user=postgres password=postgres", 50)
# Query returns List of tuples
rows = query("SELECT id, name FROM users", [])
# Execute for INSERT/UPDATE/DELETE
execute("INSERT INTO users (name) VALUES ($1)", ("Alice"))
# Check pool statistics: (active, pooled, total, max)
(active, pooled, total, max) = stats()
println("Active: " ++ show(active) ++ " Pooled: " ++ show(pooled))
Database Transactions
Use transaction for atomic operations where multiple statements must succeed or fail together.
The connection is automatically committed on success or rolled back on error.
# Transaction ensures atomicity - both updates succeed or both fail
# If any statement throws an error, the entire transaction is rolled back
transaction(conn => {
# Debit from source account
Pg.execute(conn, "UPDATE accounts SET balance = balance - $1 WHERE id = $2", (amount, fromId))
# Credit to destination account
Pg.execute(conn, "UPDATE accounts SET balance = balance + $1 WHERE id = $2", (amount, toId))
# Both statements committed together
})
# Use withConn for read-only operations that need a dedicated connection
withConn(conn => {
rows = Pg.query(conn, "SELECT * FROM large_table WHERE status = $1", "active")
# Process rows...
})
Query Parameters
Query params auto-normalize: use () for none, bare values for one param,
lists/tuples for multiple. Tuples support mixed types.
# No parameters - unit ()
query("SELECT * FROM users", ())
# Single parameter - bare value auto-wraps
query("SELECT * FROM users WHERE id = $1", userId)
# Multiple parameters - tuple supports mixed types
execute("INSERT INTO users (name, email) VALUES ($1, $2)", ("Alice", "alice@example.com"))
# Lists also work
execute("INSERT INTO data (id, name, score) VALUES ($1, $2, $3)", (1, "Bob", 95.5))
Local Lambda Pattern (DRY)
Use local lambdas to avoid repeating code. Match expressions now support comma separators for single-line style.
# Local helper lambda with inline match
handleForm(req) = {
intOr = s => match String.toInt(s) { Some(n) -> n, None -> 0 }
amount = intOr(getParam(req.formParams, "amount"))
userId = intOr(getParam(req.formParams, "user_id"))
# Use the parsed values...
}
Complete Application Code
Here is the full web application with all features integrated:
# Complete Web Application
#
# PostgreSQL + Html(...) templating + routing + forms + connection pooling
#
# Requires: PostgreSQL at localhost (user: postgres, pw: postgres)
use stdlib.html.{Html, render}
use stdlib.server.{serve, getParam, respondHtml, redirect, respond400, respond405}
use stdlib.pool.{init, initWithMax, query, execute, transaction, stats}
# --- Database Setup ---
setupDatabase() = {
execute("
CREATE TABLE IF NOT EXISTS web_users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
credits INT DEFAULT 100,
created_at TIMESTAMP DEFAULT NOW()
)
", [])
rows = query("SELECT COUNT(*) FROM web_users", [])
count = head(rows).0
if count == 0 then {
execute("INSERT INTO web_users (name, email) VALUES ($1, $2)", ("Alice", "alice@example.com"))
execute("INSERT INTO web_users (name, email) VALUES ($1, $2)", ("Bob", "bob@example.com"))
println("Inserted sample users")
} else ()
}
# --- HTML Components ---
layout(pageTitle: String, content: Html) = Html(
el("html", [], [
headEl([
meta([("charset", "UTF-8")]),
title(pageTitle ++ " - Nostos App"),
el("style", [], [raw("
body { font-family: sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; }
.container { background: white; padding: 30px; border-radius: 8px; }
nav a { margin-right: 15px; }
.card { background: #f9f9f9; padding: 15px; margin: 10px 0; border-radius: 4px; }
form input { padding: 8px; margin: 5px 0; width: 200px; }
form button { padding: 8px 16px; background: #0066cc; color: white; border: none; }
")])
]),
body([
el("div", [("class", "container")], [
nav([a([("href", "/")], "Home"), a([("href", "/users")], "Users")]),
content
])
])
])
)
userCard(userId: Int, name: String, email: String, credits: Int) = Html(
el("div", [("class", "card")], [
h3(name),
p("Email: " ++ email),
p("Credits: " ++ show(credits)),
p([a([("href", "/users/" ++ show(userId))], "View Details")])
])
)
# --- Route Handlers ---
handleHome(req) = {
rows = query("SELECT COUNT(*) FROM web_users", [])
userCount = head(rows).0
content = Html(div([
h1("Nostos Web App"),
p("Users in database: " ++ show(userCount)),
p([a([("href", "/users")], "View All Users")])
]))
respondHtml(req, render(layout("Home", content)))
}
handleUsers(req) = {
rows = query("SELECT id, name, email, credits FROM web_users ORDER BY id", [])
cards = map(rows, (id, name, email, credits) => userCard(id, name, email, credits))
content = Html(div([
h1("All Users"),
div(cards),
hr(),
h2("Add New User"),
el("form", [("method", "POST"), ("action", "/users")], [
input([("type", "text"), ("name", "name"), ("placeholder", "Name")]),
input([("type", "email"), ("name", "email"), ("placeholder", "Email")]),
el("button", [("type", "submit")], [text("Add User")])
]),
hr(),
h2("Transfer Credits"),
el("form", [("method", "POST"), ("action", "/transfer")], [
input([("type", "number"), ("name", "from"), ("placeholder", "From User ID")]),
input([("type", "number"), ("name", "to"), ("placeholder", "To User ID")]),
input([("type", "number"), ("name", "amount"), ("placeholder", "Amount")]),
el("button", [("type", "submit")], [text("Transfer")])
])
]))
respondHtml(req, render(layout("Users", content)))
}
handleUserDetail(req, userId: String) = {
intOr = s => match String.toInt(s) { Some(n) -> n, None -> 0 }
rows = query("SELECT id, name, email, credits FROM web_users WHERE id = $1", (intOr(userId)))
content = match rows {
[] -> Html(div([
h1("User Not Found"),
p([a([("href", "/users")], "Back to Users")])
]))
[(id, name, email, credits) | _] -> Html(div([
h1("User: " ++ name),
el("div", [("class", "card")], [
p("ID: " ++ show(id)),
p("Name: " ++ name),
p("Email: " ++ email),
p("Credits: " ++ show(credits))
]),
p([a([("href", "/users")], "Back to Users")])
]))
}
respondHtml(req, render(layout("User " ++ userId, content)))
}
handleCreateUser(req) = match req.method {
"POST" -> {
name = getParam(req.formParams, "name")
email = getParam(req.formParams, "email")
match (name, email) {
("", _) -> respond400(req, "Name is required")
(_, "") -> respond400(req, "Email is required")
_ -> {
execute("INSERT INTO web_users (name, email) VALUES ($1, $2)", (name, email))
redirect(req, "/users")
}
}
}
_ -> respond405(req)
}
# Transfer credits - uses transaction for atomicity
handleTransfer(req) = match req.method {
"POST" -> {
fromId = getParam(req.formParams, "from")
toId = getParam(req.formParams, "to")
amountStr = getParam(req.formParams, "amount")
match (fromId, toId, amountStr) {
("", _, _) -> respond400(req, "From user ID is required")
(_, "", _) -> respond400(req, "To user ID is required")
(_, _, "") -> respond400(req, "Amount is required")
_ -> {
intOr = s => match String.toInt(s) { Some(n) -> n, None -> 0 }
amount = intOr(amountStr)
fromIdInt = intOr(fromId)
toIdInt = intOr(toId)
if amount <= 0 then respond400(req, "Amount must be positive")
else {
transaction(conn => {
Pg.execute(conn, "UPDATE web_users SET credits = credits - $1 WHERE id = $2", (amount, fromIdInt))
Pg.execute(conn, "UPDATE web_users SET credits = credits + $1 WHERE id = $2", (amount, toIdInt))
})
redirect(req, "/users")
}
}
}
}
_ -> respond405(req)
}
handle404(req) = {
content = Html(div([h1("404 - Not Found"), p([a([("href", "/")], "Go Home")])]))
Server.respond(req.id, 404, [("Content-Type", "text/html")], render(layout("Not Found", content)))
}
# --- Router ---
route(req) = match req.path {
"/" -> handleHome(req)
"/users" -> if req.method == "POST" then handleCreateUser(req) else handleUsers(req)
"/transfer" -> handleTransfer(req)
_ -> {
params = Server.matchPath(req.path, "/users/:id")
if length(params) > 0 then {
(_, userId) = head(params)
handleUserDetail(req, userId)
} else handle404(req)
}
}
main() = {
init("host=localhost user=postgres password=postgres")
setupDatabase()
println("Web App: http://localhost:8080")
println("Using connection pooling via stdlib.pool")
serve(8080, route)
}
Key Takeaways
- Initialize pool once at startup with
init(connString)orinitWithMax(connString, 50)for custom size - Monitor pool with
stats()returning(active, pooled, total, max) - Use tuples for query parameters:
("Alice", 25)not["Alice", 25] - Wrap atomic operations in
transaction(conn => { ... })- auto commits/rollbacks - Local lambdas with inline match:
f = x => match x { A -> 1, B -> 2 } - Html components are functions that return
Html(...) - spawn-per-request happens automatically via
serve()