Skip to content

Chapter 17: Web Development ​

Build web applications entirely in Wyn - HTTP server, routing, templates, database, and deployment.

Quick Start ​

bash
wyn init myapp --web
cd myapp
wyn run
# Server running at http://localhost:8080

HTTP Server ​

wyn
fn handle(method: string, path: string, body: string, fd: int) {
    if path == "/" {
        Http.respond(fd, 200, "text/html", "<h1>Hello!</h1>")
        return
    }
    if path == "/api/data" {
        Http.respond_json(fd, 200, "{\"status\": \"ok\"}")
        return
    }
    Http.respond(fd, 404, "text/plain", "Not Found")
}

fn handle_request(req: string) {
    var fd = Http.fd(req)
    handle(Http.method(req), Http.path(req), Http.req_body(req), fd)
    Http.close_client(fd)
}

fn main() -> int {
    var server = Http.serve(8080)
    print("listening on http://localhost:8080")
    while true {
        var req = Http.accept(server)
        spawn handle_request(req)
    }
    return 0
}

Each request is handled in its own spawned task - concurrent by default.

Http.accept returns one packed record, "METHOD|PATH|BODY|TOKEN". Read it with Http.method / Http.path / Http.req_body / Http.fd rather than splitting it on "|" yourself: the body is whatever the client sent, so a pipe in it would shift every later field. An earlier edition of this chapter split by index, and curl -d 'a|b' was enough to take the server process down.

Http.fd(req) is also the value Http.respond wants. It is a capability token this process minted, not a raw file descriptor, so a client cannot ask you to write its response into someone else's connection.

Templates ​

Create templates/page.html:

html
<h1>${title}</h1>
${if:logged_in}
<p>Welcome, ${username}!</p>
<ul>
${each:items}<li>${item}</li>
${endeach}</ul>
${else}
<p>Please log in</p>
${endif}

Render from Wyn:

wyn
var ctx = { "title": "My App", "logged_in": "true", "username": "Alice" }
ctx.insert("items", "Home,About,Contact")

var html = Template.render("templates/page.html", ctx)
Http.respond(fd, 200, html)

Values are HTML-escaped by default. Use ${raw:key} for trusted HTML.

Database ​

wyn
db = Db.open("app.db")
Db.exec(db, "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")

// Always use parameterized queries - prevents SQL injection
Db.exec_p(db, "INSERT INTO users (name, email) VALUES (?, ?)", ["Alice", "[email protected]"])
users = Db.query_p(db, "SELECT name FROM users WHERE name LIKE ?", ["%ali%"])

Environment Config ​

wyn
System.load_env(".env")
port = System.env("PORT")
db_path = System.env("DATABASE_URL")

Deployment ​

Add to wyn.toml:

toml
[deploy.prod]
host = "myserver.com"
user = "deploy"
key = "~/.ssh/id_ed25519"
path = "/opt/myapp"
os = "linux"
pre = "systemctl stop myapp"
post = "systemctl start myapp"

Deploy:

bash
wyn deploy prod

One command: cross-compiles for Linux, uploads binary, restarts service. Your entire app is a single binary with no dependencies.

Exercises ​

  1. Build a URL shortener with SQLite storage
  2. Add a JSON API that returns a list of items
  3. Create a template-based blog with multiple pages

Previous: Chapter 16: Python Libraries | Next: Chapter 18: Toolchain

MIT License - v1.22.0