2026-01-26 05:12:14 +01:00
{# retoor < retoor @ molodetz . nl > #}
{% extends 'page.html' %}
2026-01-25 19:02:02 +01:00
2026-01-26 05:12:14 +01:00
{% set page_title = "Building a Web Server" %}
{% set breadcrumb = [{"url": "tutorials/index.html", "title": "Tutorials"}, {"title": "Web Server"}] %}
{% set prev_page = {"url": "tutorials/pexpect.html", "title": "Process Automation"} %}
{% set next_page = {"url": "howto/index.html", "title": "How-To Guides"} %}
{% block article %}
2026-01-25 19:02:02 +01:00
< h1 > Building a Web Server</ h1 >
< p > In this tutorial, you will build a complete web application using the < code > web</ code > module. The web module provides a high-level framework for creating HTTP servers with routing, sessions, middleware, and more.</ p >
< h2 > What You Will Learn</ h2 >
< ul >
< li > Creating a basic HTTP server</ li >
< li > Defining routes with path parameters</ li >
< li > Handling different HTTP methods</ li >
< li > Working with request data (query params, JSON, forms)</ li >
< li > Sending various response types</ li >
< li > Managing sessions and cookies</ li >
< li > Serving static files</ li >
< li > Using middleware</ li >
< li > Building class-based views</ li >
< li > Creating a REST API</ li >
< li > Making HTTP client requests</ li >
</ ul >
< h2 > Part 1: Basic Server</ h2 >
< h3 > Step 1: Hello World Server</ h3 >
< p > Create a file called < code > server.wren</ code > :</ p >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/", Fn.new { |request|
return Response.text("Hello, World!")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > Run it:</ p >
< pre >< code > $ wren_cli server.wren
Server running on http://0.0.0.0:8080</ code ></ pre >
< p > Open < code > http://localhost:8080</ code > in your browser to see the response.</ p >
< h3 > Step 2: Multiple Routes</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/", Fn.new { |request|
return Response.html("< h1> Welcome< /h1>< p> Visit < a href='/about'> About< /a>< /p> ")
})
app.get("/about", Fn.new { |request|
return Response.html("< h1> About< /h1>< p> This is a Wren web application.< /p> ")
})
app.get("/contact", Fn.new { |request|
return Response.text("Contact us at: hello@example.com")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h2 > Part 2: Request Handling</ h2 >
< h3 > Step 3: Path Parameters</ h3 >
< p > Use < code > :param</ code > syntax to capture URL segments:</ p >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/users/:id", Fn.new { |request|
var userId = request.params["id"]
return Response.text("User ID: %(userId)")
})
app.get("/posts/:category/:id", Fn.new { |request|
var category = request.params["category"]
var postId = request.params["id"]
return Response.text("Category: %(category), Post: %(postId)")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > Test with:</ p >
< pre >< code > $ curl http://localhost:8080/users/42
User ID: 42
$ curl http://localhost:8080/posts/tech/123
Category: tech, Post: 123</ code ></ pre >
< h3 > Step 4: Query Parameters</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/search", Fn.new { |request|
var query = request.query["q"] || ""
var page = request.query["page"] || "1"
var limit = request.query["limit"] || "10"
return Response.text("Searching for: %(query)\nPage: %(page), Limit: %(limit)")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > Test with:</ p >
< pre >< code > $ curl "http://localhost:8080/search?q=wren& page=2& limit=20"
Searching for: wren
Page: 2, Limit: 20</ code ></ pre >
< h3 > Step 5: Request Headers</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/headers", Fn.new { |request|
var userAgent = request.header("User-Agent") || "Unknown"
var accept = request.header("Accept") || "Unknown"
var host = request.header("Host") || "Unknown"
return Response.text("User-Agent: %(userAgent)\nAccept: %(accept)\nHost: %(host)")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Step 6: Handling POST Requests</ h3 >
< pre >< code > import "web" for Application, Response
import "json" for Json
var app = Application.new()
app.get("/form", Fn.new { |request|
var html = """
< form method="POST" action="/submit">
< input name="username" placeholder="Username">
< input name="email" placeholder="Email">
< button type="submit"> Submit< /button>
< /form>
"""
return Response.html(html)
})
app.post("/submit", Fn.new { |request|
var form = request.form
var username = form["username"]
var email = form["email"]
return Response.text("Received: %(username), %(email)")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Step 7: JSON Request Body</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.post("/api/data", Fn.new { |request|
var data = request.json
if (data == null) {
var response = Response.json({"error": "Invalid JSON"})
response.status = 400
return response
}
return Response.json({
"received": data,
"message": "Data processed successfully"
})
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > Test with:</ p >
< pre >< code > $ curl -X POST http://localhost:8080/api/data \
-H "Content-Type: application/json" \
-d '{"name": "John", "age": 30}'</ code ></ pre >
< h2 > Part 3: Response Types</ h2 >
< h3 > Step 8: Different Response Types</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/text", Fn.new { |request|
return Response.text("Plain text response")
})
app.get("/html", Fn.new { |request|
return Response.html("< h1> HTML Response< /h1>< p> With formatting.< /p> ")
})
app.get("/json", Fn.new { |request|
return Response.json({
"status": "success",
"data": [1, 2, 3],
"message": "JSON response"
})
})
app.get("/redirect", Fn.new { |request|
return Response.redirect("/")
})
app.get("/redirect-permanent", Fn.new { |request|
return Response.redirect("/new-location", 301)
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Step 9: Custom Response</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/custom", Fn.new { |request|
var response = Response.new()
response.status = 201
response.header("X-Custom-Header", "custom-value")
response.header("Content-Type", "text/plain; charset=utf-8")
response.body = "Custom response with status 201"
return response
})
app.get("/download", Fn.new { |request|
var response = Response.text("File content here")
response.header("Content-Disposition", "attachment; filename=\"data.txt\"")
return response
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Step 10: Serving Files</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/file/:name", Fn.new { |request|
var filename = request.params["name"]
var path = "./files/" + filename
return Response.file(path)
})
app.get("/image", Fn.new { |request|
return Response.file("./images/logo.png", "image/png")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > The < code > Response.file</ code > method automatically determines content types for common file extensions (.html, .css, .js, .json, .png, .jpg, etc.).</ p >
< h2 > Part 4: Static Files</ h2 >
< h3 > Step 11: Serving Static Assets</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.static_("/static", "./public")
app.get("/", Fn.new { |request|
var html = """
< !DOCTYPE html>
< html>
< head>
< link rel="stylesheet" href="/static/css/style.css">
< /head>
< body>
< h1> Welcome< /h1>
< script src="/static/js/app.js">< /script>
< /body>
< /html>
"""
return Response.html(html)
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > Create the directory structure:</ p >
< pre >< code > public/
css/
style.css
js/
app.js
images/
logo.png</ code ></ pre >
< p > Files will be served at < code > /static/css/style.css</ code > , < code > /static/js/app.js</ code > , etc.</ p >
< h2 > Part 5: Sessions and Cookies</ h2 >
< h3 > Step 12: Working with Cookies</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/set-cookie", Fn.new { |request|
var response = Response.text("Cookie set!")
response.cookie("username", "john")
response.cookie("theme", "dark", {"path": "/", "maxAge": 86400})
return response
})
app.get("/get-cookie", Fn.new { |request|
var username = request.cookies["username"] || "Guest"
var theme = request.cookies["theme"] || "light"
return Response.text("Username: %(username), Theme: %(theme)")
})
app.get("/delete-cookie", Fn.new { |request|
var response = Response.text("Cookie deleted!")
response.cookie("username", "", {"maxAge": 0})
return response
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Step 13: Session Management</ h3 >
< pre >< code > import "web" for Application, Response
var app = Application.new()
app.get("/", Fn.new { |request|
var visits = request.session["visits"]
if (visits == null) {
visits = 0
}
visits = visits + 1
request.session["visits"] = visits
return Response.text("You have visited this page %(visits) times")
})
app.get("/profile", Fn.new { |request|
var username = request.session["username"]
if (username == null) {
return Response.redirect("/login")
}
return Response.text("Welcome, %(username)!")
})
app.get("/login", Fn.new { |request|
return Response.html("""
< form method="POST" action="/login">
< input name="username" placeholder="Username">
< button> Login< /button>
< /form>
""")
})
app.post("/login", Fn.new { |request|
var username = request.form["username"]
request.session["username"] = username
return Response.redirect("/profile")
})
app.get("/logout", Fn.new { |request|
request.session["username"] = null
return Response.redirect("/")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h2 > Part 6: Middleware</ h2 >
< h3 > Step 14: Adding Middleware</ h3 >
< pre >< code > import "web" for Application, Response
import "datetime" for DateTime
var app = Application.new()
app.use(Fn.new { |request|
System.print("[%(DateTime.now())] %(request.method) %(request.path)")
return null
})
app.use(Fn.new { |request|
if (request.path.startsWith("/admin")) {
var token = request.header("Authorization")
if (token != "Bearer secret-token") {
var response = Response.json({"error": "Unauthorized"})
response.status = 401
return response
}
}
return null
})
app.get("/", Fn.new { |request|
return Response.text("Public page")
})
app.get("/admin/dashboard", Fn.new { |request|
return Response.text("Admin Dashboard")
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > Middleware functions receive the request and can return:</ p >
< ul >
< li >< code > null</ code > to continue to the next middleware/route</ li >
< li > A < code > Response</ code > object to short-circuit and send immediately</ li >
</ ul >
< h3 > Step 15: Rate Limiting Middleware</ h3 >
< pre >< code > import "web" for Application, Response
import "datetime" for DateTime
var requestCounts = {}
var rateLimiter = Fn.new { |request|
var ip = request.header("X-Forwarded-For") || "unknown"
var now = DateTime.now().timestamp
var windowStart = now - 60
if (!requestCounts.containsKey(ip)) {
requestCounts[ip] = []
}
var requests = requestCounts[ip].where { |t| t > windowStart }.toList
requests.add(now)
requestCounts[ip] = requests
if (requests.count > 100) {
var response = Response.json({"error": "Rate limit exceeded"})
response.status = 429
return response
}
return null
}
var app = Application.new()
app.use(rateLimiter)
app.get("/api/data", Fn.new { |request|
return Response.json({"message": "Success"})
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h2 > Part 7: Class-Based Views</ h2 >
< h3 > Step 16: Using View Classes</ h3 >
< pre >< code > import "web" for Application, Response, View
class UserView is View {
get(request) {
var userId = request.params["id"]
return Response.json({
"id": userId,
"name": "User %(userId)",
"email": "user%(userId)@example.com"
})
}
post(request) {
var data = request.json
return Response.json({
"message": "User created",
"data": data
})
}
put(request) {
var userId = request.params["id"]
var data = request.json
return Response.json({
"message": "User %(userId) updated",
"data": data
})
}
delete(request) {
var userId = request.params["id"]
return Response.json({
"message": "User %(userId) deleted"
})
}
}
var app = Application.new()
app.addView("/users/:id", UserView)
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > The < code > View</ code > base class automatically routes HTTP methods to the appropriate class method. Unsupported methods return 405 Method Not Allowed.</ p >
< h2 > Part 8: Building a REST API</ h2 >
< h3 > Step 17: Complete CRUD API</ h3 >
< pre >< code > import "web" for Application, Response
import "uuid" for Uuid
var items = {}
var app = Application.new()
app.get("/api/items", Fn.new { |request|
var itemList = []
for (id in items.keys) {
itemList.add(items[id])
}
return Response.json({"items": itemList, "count": itemList.count})
})
app.get("/api/items/:id", Fn.new { |request|
var id = request.params["id"]
if (!items.containsKey(id)) {
var response = Response.json({"error": "Item not found"})
response.status = 404
return response
}
return Response.json(items[id])
})
app.post("/api/items", Fn.new { |request|
var data = request.json
if (data == null || !data.containsKey("name")) {
var response = Response.json({"error": "Name is required"})
response.status = 400
return response
}
var id = Uuid.v4()
var item = {
"id": id,
"name": data["name"],
"description": data["description"] || ""
}
items[id] = item
var response = Response.json(item)
response.status = 201
return response
})
app.put("/api/items/:id", Fn.new { |request|
var id = request.params["id"]
var data = request.json
if (!items.containsKey(id)) {
var response = Response.json({"error": "Item not found"})
response.status = 404
return response
}
var item = items[id]
if (data.containsKey("name")) item["name"] = data["name"]
if (data.containsKey("description")) item["description"] = data["description"]
items[id] = item
return Response.json(item)
})
app.delete("/api/items/:id", Fn.new { |request|
var id = request.params["id"]
if (!items.containsKey(id)) {
var response = Response.json({"error": "Item not found"})
response.status = 404
return response
}
items.remove(id)
var response = Response.new()
response.status = 204
return response
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h2 > Part 9: HTTP Client</ h2 >
< h3 > Step 18: Making HTTP Requests</ h3 >
< p > The web module also includes a < code > Client</ code > class for making HTTP requests:</ p >
< pre >< code > import "web" for Client
var response = Client.get("https://api.example.com/data")
System.print("Status: %(response["status"])")
System.print("Body: %(response["body"])")</ code ></ pre >
< h3 > Step 19: POST Requests with JSON</ h3 >
< pre >< code > import "web" for Client
var response = Client.post("https://api.example.com/users", {
"json": {
"name": "John Doe",
"email": "john@example.com"
}
})
System.print("Status: %(response["status"])")
System.print("Response: %(response["body"])")</ code ></ pre >
< h3 > Step 20: Custom Headers</ h3 >
< pre >< code > import "web" for Client
var response = Client.get("https://api.example.com/protected", {
"headers": {
"Authorization": "Bearer my-token",
"Accept": "application/json"
}
})
System.print("Data: %(response["body"])")</ code ></ pre >
< h2 > Part 10: Complete Application</ h2 >
< h3 > Step 21: Full-Featured Web Application</ h3 >
< pre >< code > import "web" for Application, Response, View
import "jinja" for Jinja
import "uuid" for Uuid
import "datetime" for DateTime
class TaskView is View {
construct new() {
super()
_tasks = {}
}
get(request) {
var id = request.params["id"]
if (id == null) {
var taskList = []
for (taskId in _tasks.keys) {
taskList.add(_tasks[taskId])
}
return Response.json({"tasks": taskList})
}
if (!_tasks.containsKey(id)) {
var response = Response.json({"error": "Task not found"})
response.status = 404
return response
}
return Response.json(_tasks[id])
}
post(request) {
var data = request.json
var id = Uuid.v4()
var task = {
"id": id,
"title": data["title"],
"completed": false,
"createdAt": DateTime.now().toString
}
_tasks[id] = task
var response = Response.json(task)
response.status = 201
return response
}
put(request) {
var id = request.params["id"]
var data = request.json
if (!_tasks.containsKey(id)) {
var response = Response.json({"error": "Task not found"})
response.status = 404
return response
}
var task = _tasks[id]
if (data.containsKey("title")) task["title"] = data["title"]
if (data.containsKey("completed")) task["completed"] = data["completed"]
_tasks[id] = task
return Response.json(task)
}
delete(request) {
var id = request.params["id"]
if (!_tasks.containsKey(id)) {
var response = Response.json({"error": "Task not found"})
response.status = 404
return response
}
_tasks.remove(id)
var response = Response.new()
response.status = 204
return response
}
}
var tasks = {}
var app = Application.new()
app.use(Fn.new { |request|
System.print("[%(DateTime.now())] %(request.method) %(request.path)")
return null
})
app.static_("/static", "./public")
app.get("/", Fn.new { |request|
var html = """
< !DOCTYPE html>
< html>
< head>
< title> Task Manager< /title>
< style>
body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.task { padding: 10px; border: 1px solid #ddd; margin: 5px 0; }
.completed { text-decoration: line-through; opacity: 0.6; }
< /style>
< /head>
< body>
< h1> Task Manager< /h1>
< form id="taskForm">
< input id="title" placeholder="New task..." required>
< button type="submit"> Add< /button>
< /form>
< div id="tasks">< /div>
< script>
async function loadTasks() {
const res = await fetch('/api/tasks');
const data = await res.json();
const html = data.tasks.map(t =>
'< div class="task ' + (t.completed ? 'completed' : '') + '"> ' +
t.title + ' < button onclick="toggleTask(\\'' + t.id + '\\', ' + !t.completed + ')"> Toggle< /button> ' +
' < button onclick="deleteTask(\\'' + t.id + '\\')"> Delete< /button>< /div> '
).join('');
document.getElementById('tasks').innerHTML = html;
}
document.getElementById('taskForm').onsubmit = async (e) => {
e.preventDefault();
const title = document.getElementById('title').value;
await fetch('/api/tasks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title})
});
document.getElementById('title').value = '';
loadTasks();
};
async function toggleTask(id, completed) {
await fetch('/api/tasks/' + id, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({completed})
});
loadTasks();
}
async function deleteTask(id) {
await fetch('/api/tasks/' + id, {method: 'DELETE'});
loadTasks();
}
loadTasks();
< /script>
< /body>
< /html>
"""
return Response.html(html)
})
app.get("/api/tasks", Fn.new { |request|
var taskList = []
for (id in tasks.keys) {
taskList.add(tasks[id])
}
return Response.json({"tasks": taskList})
})
app.get("/api/tasks/:id", Fn.new { |request|
var id = request.params["id"]
if (!tasks.containsKey(id)) {
var response = Response.json({"error": "Not found"})
response.status = 404
return response
}
return Response.json(tasks[id])
})
app.post("/api/tasks", Fn.new { |request|
var data = request.json
var id = Uuid.v4()
tasks[id] = {
"id": id,
"title": data["title"],
"completed": false
}
var response = Response.json(tasks[id])
response.status = 201
return response
})
app.put("/api/tasks/:id", Fn.new { |request|
var id = request.params["id"]
var data = request.json
if (!tasks.containsKey(id)) {
var response = Response.json({"error": "Not found"})
response.status = 404
return response
}
var task = tasks[id]
if (data.containsKey("title")) task["title"] = data["title"]
if (data.containsKey("completed")) task["completed"] = data["completed"]
return Response.json(task)
})
app.delete("/api/tasks/:id", Fn.new { |request|
var id = request.params["id"]
tasks.remove(id)
var response = Response.new()
response.status = 204
return response
})
System.print("Task Manager running at http://localhost:8080")
app.run("0.0.0.0", 8080)</ code ></ pre >
< div class = "admonition note" >
< div class = "admonition-title" > Note</ div >
< p > The web module handles HTTP request parsing, routing, and response building automatically. Sessions are managed via cookies with UUID-based session IDs.</ p >
</ div >
< h2 > Part 11: WebSocket Support</ h2 >
< h3 > Step 22: Adding WebSocket Endpoints</ h3 >
< p > The web module supports WebSocket connections using an aiohttp-style API:</ p >
< pre >< code > import "web" for Application, Response, WebSocketResponse
var app = Application.new()
app.get("/", Fn.new { |request|
var html = """
< !DOCTYPE html>
< html>
< body>
< h1> WebSocket Echo< /h1>
< input id="msg" placeholder="Type a message...">
< button onclick="send()"> Send< /button>
< div id="output">< /div>
< script>
var ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = function(e) {
document.getElementById('output').innerHTML += '< p> ' + e.data + '< /p> ';
};
function send() {
ws.send(document.getElementById('msg').value);
document.getElementById('msg').value = '';
}
< /script>
< /body>
< /html>
"""
return Response.html(html)
})
app.websocket("/ws", Fn.new { |request|
var ws = WebSocketResponse.new()
ws.prepare(request)
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isText) {
ws.send("Echo: " + msg.text)
}
}
ws.close()
return ws
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Step 23: WebSocket in Class-Based Views</ h3 >
< p > Views can also handle WebSocket connections by overriding the < code > websocket</ code > method:</ p >
< pre >< code > import "web" for Application, Response, View, WebSocketResponse
class ChatView is View {
get(request) {
return Response.html("< h1> Chat Room< /h1> ")
}
websocket(request) {
var ws = WebSocketResponse.new()
ws.prepare(request)
ws.send("Welcome to the chat!")
ws.iterate(Fn.new { |msg|
if (msg.isText) {
ws.send("You said: " + msg.text)
}
})
return ws
}
}
var app = Application.new()
app.addView("/chat", ChatView)
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > The < code > iterate</ code > helper method loops while the connection is open and calls your callback for each message (excluding close frames).</ p >
< h3 > Step 24: JSON Messaging Pattern</ h3 >
< p > For structured communication, use the JSON convenience methods:</ p >
< pre >< code > import "web" for Application, Response, WebSocketResponse
var app = Application.new()
app.websocket("/api", Fn.new { |request|
var ws = WebSocketResponse.new()
ws.prepare(request)
ws.sendJson({"type": "connected", "version": "1.0"})
while (ws.isOpen) {
var data = ws.receiveJson()
if (data == null) break
if (data is Map && data.containsKey("type")) {
if (data["type"] == "ping") {
ws.sendJson({"type": "pong", "timestamp": data["timestamp"]})
} else if (data["type"] == "subscribe") {
ws.sendJson({"type": "subscribed", "channel": data["channel"]})
} else if (data["type"] == "message") {
ws.sendJson({"type": "ack", "id": data["id"]})
}
}
}
return ws
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< p > The < code > sendJson</ code > method automatically serializes data to JSON, and < code > receiveJson</ code > parses incoming text messages as JSON.</ p >
< h3 > Step 25: Binary Data Transfer</ h3 >
< p > WebSocket supports binary data for efficient file transfers:</ p >
< pre >< code > import "web" for Application, Response, WebSocketResponse
import "io" for File
var app = Application.new()
app.websocket("/files", Fn.new { |request|
var ws = WebSocketResponse.new()
ws.prepare(request)
while (ws.isOpen) {
var msg = ws.receive()
if (msg == null || msg.isClose) break
if (msg.isBinary) {
var bytes = msg.bytes
System.print("Received %(bytes.count) bytes")
File.writeBytes("upload.bin", bytes)
ws.sendJson({"status": "ok", "size": bytes.count})
} else if (msg.isText && msg.text == "download") {
var data = File.readBytes("download.bin")
ws.sendBinary(data)
}
}
return ws
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< h3 > Step 26: Broadcast Pattern</ h3 >
< p > To broadcast messages to multiple clients, maintain a list of connections:</ p >
< pre >< code > import "web" for Application, Response, WebSocketResponse
import "json" for Json
var clients = []
var app = Application.new()
app.get("/", Fn.new { |request|
return Response.html("""
< !DOCTYPE html>
< html>
< body>
< h1> Chat< /h1>
< input id="msg" placeholder="Message...">
< button onclick="send()"> Send< /button>
< div id="messages">< /div>
< script>
var ws = new WebSocket('ws://localhost:8080/chat');
ws.onmessage = function(e) {
var data = JSON.parse(e.data);
document.getElementById('messages').innerHTML +=
'< p>< b> ' + data.from + ':< /b> ' + data.text + '< /p> ';
};
function send() {
ws.send(JSON.stringify({
from: 'User',
text: document.getElementById('msg').value
}));
document.getElementById('msg').value = '';
}
< /script>
< /body>
< /html>
""")
})
app.websocket("/chat", Fn.new { |request|
var ws = WebSocketResponse.new()
ws.prepare(request)
clients.add(ws)
System.print("Client connected. Total: %(clients.count)")
while (ws.isOpen) {
var data = ws.receiveJson()
if (data == null) break
if (data is Map) {
for (client in clients) {
if (client.isOpen) {
client.sendJson(data)
}
}
}
}
clients = clients.where { |c| c != ws }.toList
System.print("Client disconnected. Total: %(clients.count)")
return ws
})
app.run("0.0.0.0", 8080)</ code ></ pre >
< div class = "admonition tip" >
< div class = "admonition-title" > Tip</ div >
< p > For production chat applications, consider using a message queue or pub/sub system to handle message distribution across multiple server instances.</ p >
</ div >
< h2 > Request Object Reference</ h2 >
< table >
< thead >
< tr >
< th > Property</ th >
< th > Description</ th >
</ tr >
</ thead >
< tbody >
< tr >
< td >< code > request.method</ code ></ td >
< td > HTTP method (GET, POST, etc.)</ td >
</ tr >
< tr >
< td >< code > request.path</ code ></ td >
< td > URL path without query string</ td >
</ tr >
< tr >
< td >< code > request.query</ code ></ td >
< td > Map of query parameters</ td >
</ tr >
< tr >
< td >< code > request.headers</ code ></ td >
< td > Map of request headers</ td >
</ tr >
< tr >
< td >< code > request.header(name)</ code ></ td >
< td > Get header (case-insensitive)</ td >
</ tr >
< tr >
< td >< code > request.body</ code ></ td >
< td > Raw request body string</ td >
</ tr >
< tr >
< td >< code > request.json</ code ></ td >
< td > Parsed JSON body</ td >
</ tr >
< tr >
< td >< code > request.form</ code ></ td >
< td > Parsed form data</ td >
</ tr >
< tr >
< td >< code > request.params</ code ></ td >
< td > URL path parameters</ td >
</ tr >
< tr >
< td >< code > request.cookies</ code ></ td >
< td > Map of cookies</ td >
</ tr >
< tr >
< td >< code > request.session</ code ></ td >
< td > Session object</ td >
</ tr >
</ tbody >
</ table >
< h2 > Response Factory Methods</ h2 >
< table >
< thead >
< tr >
< th > Method</ th >
< th > Description</ th >
</ tr >
</ thead >
< tbody >
< tr >
< td >< code > Response.text(content)</ code ></ td >
< td > Plain text response</ td >
</ tr >
< tr >
< td >< code > Response.html(content)</ code ></ td >
< td > HTML response</ td >
</ tr >
< tr >
< td >< code > Response.json(data)</ code ></ td >
< td > JSON response</ td >
</ tr >
< tr >
< td >< code > Response.file(path)</ code ></ td >
< td > File response</ td >
</ tr >
< tr >
< td >< code > Response.redirect(url)</ code ></ td >
< td > 302 redirect</ td >
</ tr >
< tr >
< td >< code > Response.redirect(url, status)</ code ></ td >
< td > Redirect with status</ td >
</ tr >
</ tbody >
</ table >
< div class = "admonition tip" >
< div class = "admonition-title" > Tip</ div >
< p > For production use, consider adding proper error handling, input validation, and connecting to a database. See the < a href = "database-app.html" > Database App</ a > tutorial for SQLite integration.</ p >
</ div >
< h2 > Next Steps</ h2 >
< ul >
< li > Add database persistence with < a href = "database-app.html" > SQLite</ a ></ li >
< li > Use < a href = "template-rendering.html" > Jinja templates</ a > for HTML rendering</ li >
< li > Add real-time features with < a href = "websocket-chat.html" > WebSockets</ a ></ li >
< li > Explore the < a href = "../api/http.html" > HTTP module</ a > for lower-level control</ li >
</ ul >
2026-01-26 05:12:14 +01:00
{% endblock %}