Your second project — A web server
Putting Part 2 together
In Part 1, you built a student grade tracker. Now you know modules, files, closures, enums, traits, generics, networking, and concurrency. Time to put it all together and build something real: a multi-threaded web server that serves dynamic pages.
We will build a personal bookshelf application: users can view books, add new ones, and see statistics — all through a web browser.
Step 1: Define the data
struct Book { title: String, author: String, pages: int, read: bool } fn make_book(title: String, author: String, pages: int) -> Book { return Book { title: title, author: author, pages: pages, read: false } }
Step 2: Build the book store
We will keep books in an array and provide functions to work with them:
fn count_read(books: Array) -> int { var count: int = 0 var i: int = 0 while i < books.length() { let b: Book = books[i] if b.read { count += 1 } i += 1 } return count } fn total_pages(books: Array) -> int { var total: int = 0 var i: int = 0 while i < books.length() { let b: Book = books[i] total += b.pages i += 1 } return total } fn find_longest(books: Array) -> String { if books.length() == 0 { return "None" } var best: String = "" var best_pages: int = 0 var i: int = 0 while i < books.length() { let b: Book = books[i] if b.pages > best_pages { best_pages = b.pages best = b.title } i += 1 } return best }
Step 3: Build HTML pages
Let's create functions that generate HTML:
fn html_page(title: String, body: String) -> String { return "<!DOCTYPE html><html><head><title>" + title + "</title>" + "<style>body{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;}" + "h1{color:#333;}table{width:100%;border-collapse:collapse;}" + "th,td{padding:8px;text-align:left;border-bottom:1px solid #ddd;}" + "a{color:#0066cc;text-decoration:none;}a:hover{text-decoration:underline;}" + ".stat{background:#f5f5f5;padding:15px;margin:10px 0;border-radius:5px;}" + "</style></head><body>" + body + "</body></html>" } fn nav_bar() -> String { return "<nav><a href=\"/\">Home</a> | <a href=\"/books\">Books</a> | <a href=\"/stats\">Stats</a></nav><hr>" }
Step 4: The books list page
fn books_page(books: Array) -> String { var rows: String = "" var i: int = 0 while i < books.length() { let b: Book = books[i] let status: String = match b.read { true => "Yes", false => "No" } rows = rows + "<tr><td>" + b.title + "</td><td>" + b.author + "</td><td>" + int_to_string(b.pages) + "</td><td>" + status + "</td></tr>" i += 1 } let body: String = nav_bar() + "<h1>My Bookshelf</h1>" + "<table><tr><th>Title</th><th>Author</th><th>Pages</th><th>Read?</th></tr>" + rows + "</table>" + "<br><p><a href=\"/add\">+ Add a book</a></p>" return html_page("Books", body) }
Step 5: The stats page
fn stats_page(books: Array) -> String { let total: int = books.length() let read: int = count_read(books) let unread: int = total - read let pages: int = total_pages(books) let longest: String = find_longest(books) let body: String = nav_bar() + "<h1>Reading Statistics</h1>" + "<div class=\"stat\">Total books: " + int_to_string(total) + "</div>" + "<div class=\"stat\">Read: " + int_to_string(read) + " / Unread: " + int_to_string(unread) + "</div>" + "<div class=\"stat\">Total pages: " + int_to_string(pages) + "</div>" + "<div class=\"stat\">Longest book: " + longest + "</div>" return html_page("Stats", body) }
Step 6: The add book form
fn add_page() -> String { let body: String = nav_bar() + "<h1>Add a Book</h1>" + "<form method=\"POST\" action=\"/add\">" + "<p>Title: <input name=\"title\" required></p>" + "<p>Author: <input name=\"author\" required></p>" + "<p>Pages: <input name=\"pages\" type=\"number\" required></p>" + "<p><button type=\"submit\">Add Book</button></p>" + "</form>" return html_page("Add Book", body) }
Step 7: Parse form data
When the user submits the form, the browser sends a POST request with form data. Let's parse it:
fn parse_form(body: String) -> Map { var result: Map = Map.new() let pairs: Array = body.split("&") var i: int = 0 while i < pairs.length() { let pair: String = pairs[i] let eq: int = pair.indexOf("=") if eq > 0 { let key: String = pair.substring(0, eq) let value: String = pair.substring(eq + 1, pair.length()) result.insert(key, value) } i += 1 } return result }
Step 8: The request router
Now let's wire everything together with a request handler:
import { http_serve_mt, http_response, http_response_with_headers } from "std/http" // Global book list (shared across requests) var books: Array = [] fn setup_sample_books() { books.push(make_book("The Pragmatic Programmer", "Hunt & Thomas", 352)) books.push(make_book("Structure and Interpretation", "Abelson & Sussman", 657)) books.push(make_book("The C Programming Language", "Kernighan & Ritchie", 288)) // Mark one as read let b: Book = books[0] books[0] = Book { title: b.title, author: b.author, pages: b.pages, read: true } } fn on_request(request: Array) -> String { let method: String = request[1] let path: String = request[2] let body: String = request[4] let html_headers: Array = ["Content-Type", "text/html; charset=utf-8"] if path == "/" { let home_body: String = nav_bar() + "<h1>Welcome to My Bookshelf</h1>" + "<p>A personal book tracker built with Nyx.</p>" + "<p>You have <strong>" + int_to_string(books.length()) + "</strong> books.</p>" + "<p><a href=\"/books\">View all books</a></p>" let html: String = html_page("Home", home_body) return http_response_with_headers(200, html_headers, html) } if path == "/books" { let html: String = books_page(books) return http_response_with_headers(200, html_headers, html) } if path == "/stats" { let html: String = stats_page(books) return http_response_with_headers(200, html_headers, html) } if path == "/add" and method == "GET" { let html: String = add_page() return http_response_with_headers(200, html_headers, html) } if path == "/add" and method == "POST" { let form: Map = parse_form(body) if form.contains("title") and form.contains("author") and form.contains("pages") { let title: String = form.get("title") let author: String = form.get("author") let pages: int = string_to_int(form.get("pages")) books.push(make_book(title, author, pages)) } // Redirect to books list let redirect_headers: Array = ["Location", "/books"] return http_response_with_headers(302, redirect_headers, "") } return http_response(404, "Page not found") }
The complete program
struct Book { title: String, author: String, pages: int, read: bool } fn make_book(title: String, author: String, pages: int) -> Book { return Book { title: title, author: author, pages: pages, read: false } } fn count_read(books: Array) -> int { var count: int = 0 var i: int = 0 while i < books.length() { let b: Book = books[i] if b.read { count += 1 } i += 1 } return count } fn total_pages(books: Array) -> int { var total: int = 0 var i: int = 0 while i < books.length() { let b: Book = books[i] total += b.pages i += 1 } return total } fn find_longest(books: Array) -> String { if books.length() == 0 { return "None" } var best: String = "" var best_pages: int = 0 var i: int = 0 while i < books.length() { let b: Book = books[i] if b.pages > best_pages { best_pages = b.pages best = b.title } i += 1 } return best } fn html_page(title: String, body: String) -> String { return "<!DOCTYPE html><html><head><title>" + title + "</title>" + "<style>body{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;}" + "h1{color:#333;}table{width:100%;border-collapse:collapse;}" + "th,td{padding:8px;text-align:left;border-bottom:1px solid #ddd;}" + "a{color:#0066cc;}a:hover{text-decoration:underline;}" + ".stat{background:#f5f5f5;padding:15px;margin:10px 0;border-radius:5px;}" + "</style></head><body>" + body + "</body></html>" } fn nav_bar() -> String { return "<nav><a href=\"/\">Home</a> | <a href=\"/books\">Books</a> | <a href=\"/stats\">Stats</a></nav><hr>" } fn books_page(bks: Array) -> String { var rows: String = "" var i: int = 0 while i < bks.length() { let b: Book = bks[i] var status: String = "No" if b.read { status = "Yes" } rows = rows + "<tr><td>" + b.title + "</td><td>" + b.author + "</td><td>" + int_to_string(b.pages) + "</td><td>" + status + "</td></tr>" i += 1 } let body: String = nav_bar() + "<h1>My Bookshelf</h1>" + "<table><tr><th>Title</th><th>Author</th><th>Pages</th><th>Read?</th></tr>" + rows + "</table><br><p><a href=\"/add\">+ Add a book</a></p>" return html_page("Books", body) } fn stats_page(bks: Array) -> String { let total: int = bks.length() let rd: int = count_read(bks) let pages: int = total_pages(bks) let longest: String = find_longest(bks) let body: String = nav_bar() + "<h1>Reading Statistics</h1>" + "<div class=\"stat\">Total books: " + int_to_string(total) + "</div>" + "<div class=\"stat\">Read: " + int_to_string(rd) + " / Unread: " + int_to_string(total - rd) + "</div>" + "<div class=\"stat\">Total pages: " + int_to_string(pages) + "</div>" + "<div class=\"stat\">Longest book: " + longest + "</div>" return html_page("Stats", body) } fn add_page() -> String { let body: String = nav_bar() + "<h1>Add a Book</h1>" + "<form method=\"POST\" action=\"/add\">" + "<p>Title: <input name=\"title\" required></p>" + "<p>Author: <input name=\"author\" required></p>" + "<p>Pages: <input name=\"pages\" type=\"number\" required></p>" + "<p><button type=\"submit\">Add Book</button></p></form>" return html_page("Add Book", body) } fn parse_form(body: String) -> Map { var result: Map = Map.new() let pairs: Array = body.split("&") var i: int = 0 while i < pairs.length() { let pair: String = pairs[i] let eq: int = pair.indexOf("=") if eq > 0 { let key: String = pair.substring(0, eq) let value: String = pair.substring(eq + 1, pair.length()) result.insert(key, value) } i += 1 } return result } import { http_serve_mt, http_response, http_response_with_headers } from "std/http" var books: Array = [] fn on_request(request: Array) -> String { let method: String = request[1] let path: String = request[2] let body: String = request[4] let html_headers: Array = ["Content-Type", "text/html; charset=utf-8"] if path == "/" { let home: String = nav_bar() + "<h1>Welcome to My Bookshelf</h1>" + "<p>A personal book tracker built with Nyx.</p>" + "<p>You have <strong>" + int_to_string(books.length()) + "</strong> books.</p>" + "<p><a href=\"/books\">View all books</a></p>" return http_response_with_headers(200, html_headers, html_page("Home", home)) } if path == "/books" { return http_response_with_headers(200, html_headers, books_page(books)) } if path == "/stats" { return http_response_with_headers(200, html_headers, stats_page(books)) } if path == "/add" and method == "GET" { return http_response_with_headers(200, html_headers, add_page()) } if path == "/add" and method == "POST" { let form: Map = parse_form(body) if form.contains("title") and form.contains("author") and form.contains("pages") { books.push(make_book(form.get("title"), form.get("author"), string_to_int(form.get("pages")))) } let redir: Array = ["Location", "/books"] return http_response_with_headers(302, redir, "") } return http_response(404, "Page not found") } fn main() { // Seed with sample data books.push(make_book("The Pragmatic Programmer", "Hunt and Thomas", 352)) books.push(make_book("Structure and Interpretation", "Abelson and Sussman", 657)) books.push(make_book("The C Programming Language", "Kernighan and Ritchie", 288)) print("Bookshelf server running on http://localhost:8080") print("Open your browser and visit http://localhost:8080") http_serve_mt(8080, 4, on_request) }
What you built
This program uses everything from Part 2:
- Modules — importing from
std/http. - Structs —
Bookto model data. - Arrays — storing the book collection.
- Maps — parsing form data.
- Strings — building HTML dynamically.
- Functions — each page and utility is its own function.
- Control flow — routing requests by path and method.
- Concurrency —
http_serve_mthandles multiple clients with threads.
Open http://localhost:8080 in your browser and you have a working web application — built entirely in Nyx, compiled to native code, running at thousands of requests per second.
Challenges
- Mark as read: Add a
/read/Nroute that marks book N as read and redirects to/books.
- Delete books: Add a
/delete/Nroute that removes book N from the list.
- Search: Add a
/search?q=termroute that shows only books matching the search term.
- Persistence: Save books to a file (
books.csv) on every change and load them on startup.
- JSON API: Add
/api/booksthat returns the book list as JSON, and/api/books/addthat accepts JSON POST requests.
What's next?
Congratulations! You have completed Part 2 of The Nyx Book. You can now build real, networked applications: modules for organization, files for persistence, closures for flexibility, enums for safety, traits for abstraction, generics for reuse, networking for communication, and concurrency for performance.
In Part 3, you will go deeper: LLVM internals, FFI to call C code, unsafe operations, async I/O, and a case study of how nyx-kv (a Redis-compatible database) was built entirely in Nyx.
Next chapter: LLVM and performance →