Chapter 17: Web Development
Build web applications entirely in Wyn - HTTP server, routing, templates, database, and deployment.
Quick Start
wyn init myapp --web
cd myapp
wyn run
# Server running at http://localhost:8080HTTP Server
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:
<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:
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
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
System.load_env(".env")
port = System.env("PORT")
db_path = System.env("DATABASE_URL")Deployment
Add to wyn.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:
wyn deploy prodOne command: cross-compiles for Linux, uploads binary, restarts service. Your entire app is a single binary with no dependencies.
Exercises
- Build a URL shortener with SQLite storage
- Add a JSON API that returns a list of items
- Create a template-based blog with multiple pages
Previous: Chapter 16: Python Libraries | Next: Chapter 18: Toolchain