Created
August 1, 2025 02:25
-
-
Save naranyala/b169d8423c5b171648b50aa575b0527a to your computer and use it in GitHub Desktop.
your ready-to-go nim web server with jester, htmx, and bootstrap.css
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # nimble install jester | |
| # nimble install sqlite3_abi | |
| import jester | |
| import sqlite3_abi | |
| import strutils | |
| import os | |
| import locks | |
| import logging | |
| const | |
| DB_PATH = "app.db" | |
| PORT = 5000 | |
| type | |
| Database = ptr sqlite3 | |
| DbError = object of IOError | |
| # Global database with lock | |
| var | |
| dbLock: Lock | |
| globalDB: Database | |
| proc connectDB(): Database = | |
| var db: Database | |
| info "Opening database connection: ", DB_PATH | |
| if sqlite3_open_v2(DB_PATH.cstring, addr db, SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE or SQLITE_OPEN_FULLMUTEX, nil) != SQLITE_OK: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to open database: ", err | |
| if db != nil: discard sqlite3_close(db) | |
| raise newException(DbError, "Cannot open database: " & err) | |
| info "Database connection opened successfully" | |
| return db | |
| proc closeDB(db: Database) = | |
| if db != nil: | |
| info "Closing database connection" | |
| discard sqlite3_close(db) | |
| proc execSQL(db: Database, sql: string) = | |
| if db == nil: | |
| error "Database connection is nil in execSQL" | |
| raise newException(DbError, "Database connection is nil") | |
| var errMsg: cstring | |
| info "Executing SQL: ", sql[0..min(sql.len-1, 50)] | |
| if sqlite3_exec(db, sql.cstring, nil, nil, addr errMsg) != SQLITE_OK: | |
| let err = if errMsg != nil: $errMsg else: "Unknown SQL error" | |
| error "SQL error: ", err | |
| if errMsg != nil: sqlite3_free(errMsg) | |
| raise newException(DbError, "SQL error: " & err) | |
| proc initDB(db: Database) = | |
| const createTable = """ | |
| CREATE TABLE IF NOT EXISTS messages ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| content TEXT NOT NULL | |
| ) | |
| """ | |
| info "Initializing database schema" | |
| execSQL(db, createTable) | |
| info "Database schema initialized" | |
| proc insertMessage(db: Database, content: string) = | |
| if db == nil: | |
| error "Database connection is nil in insertMessage" | |
| raise newException(DbError, "Database connection is nil") | |
| const sql = "INSERT INTO messages (content) VALUES (?)" | |
| var stmt: ptr sqlite3_stmt | |
| info "Preparing insert statement for content: ", content[0..min(content.len-1, 50)] | |
| if sqlite3_prepare_v2(db, sql.cstring, -1, addr stmt, nil) != SQLITE_OK: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to prepare statement: ", err | |
| raise newException(DbError, "Failed to prepare statement: " & err) | |
| defer: discard sqlite3_finalize(stmt) | |
| if sqlite3_bind_text(stmt, 1, content.cstring, content.len.cint, nil) != SQLITE_OK: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to bind parameter: ", err | |
| raise newException(DbError, "Failed to bind parameter: " & err) | |
| if sqlite3_step(stmt) != SQLITE_DONE: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to execute statement: ", err | |
| raise newException(DbError, "Failed to execute statement: " & err) | |
| info "Message inserted successfully" | |
| proc getMessages(db: Database): seq[tuple[id: int, content: string]] = | |
| if db == nil: | |
| error "Database connection is nil in getMessages" | |
| raise newException(DbError, "Database connection is nil") | |
| result = @[] | |
| const sql = "SELECT id, content FROM messages" | |
| var stmt: ptr sqlite3_stmt | |
| info "Preparing select statement" | |
| if sqlite3_prepare_v2(db, sql.cstring, -1, addr stmt, nil) != SQLITE_OK: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to prepare statement: ", err | |
| raise newException(DbError, "Failed to prepare statement: " & err) | |
| defer: discard sqlite3_finalize(stmt) | |
| while sqlite3_step(stmt) == SQLITE_ROW: | |
| let id = sqlite3_column_int(stmt, 0).int | |
| let text = sqlite3_column_text(stmt, 1) | |
| if text != nil: | |
| result.add((id: id, content: $text)) | |
| info "Retrieved ", result.len, " messages" | |
| proc deleteMessage(db: Database, id: int) = | |
| if db == nil: | |
| error "Database connection is nil in deleteMessage" | |
| raise newException(DbError, "Database connection is nil") | |
| const sql = "DELETE FROM messages WHERE id = ?" | |
| var stmt: ptr sqlite3_stmt | |
| info "Preparing delete statement for id: ", id | |
| if sqlite3_prepare_v2(db, sql.cstring, -1, addr stmt, nil) != SQLITE_OK: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to prepare statement: ", err | |
| raise newException(DbError, "Failed to prepare statement: " & err) | |
| defer: discard sqlite3_finalize(stmt) | |
| if sqlite3_bind_int(stmt, 1, id.cint) != SQLITE_OK: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to bind parameter: ", err | |
| raise newException(DbError, "Failed to bind parameter: " & err) | |
| if sqlite3_step(stmt) != SQLITE_DONE: | |
| let err = $sqlite3_errmsg(db) | |
| error "Failed to execute statement: ", err | |
| raise newException(DbError, "Failed to execute statement: " & err) | |
| info "Message deleted successfully" | |
| proc safeInsertMessage(content: string) = | |
| withLock dbLock: | |
| if globalDB == nil: | |
| error "globalDB is nil in safeInsertMessage" | |
| raise newException(DbError, "Database connection is nil") | |
| insertMessage(globalDB, content) | |
| proc safeGetMessages(): seq[tuple[id: int, content: string]] = | |
| withLock dbLock: | |
| if globalDB == nil: | |
| error "globalDB is nil in safeGetMessages" | |
| raise newException(DbError, "Database connection is nil") | |
| return getMessages(globalDB) | |
| proc safeDeleteMessage(id: int) = | |
| withLock dbLock: | |
| if globalDB == nil: | |
| error "globalDB is nil in safeDeleteMessage" | |
| raise newException(DbError, "Database connection is nil") | |
| deleteMessage(globalDB, id) | |
| proc setupDatabase() = | |
| addHandler(newConsoleLogger(lvlAll)) | |
| info "Initializing database" | |
| initLock(dbLock) | |
| try: | |
| globalDB = connectDB() | |
| if globalDB == nil: | |
| error "Failed to initialize database: connection is nil" | |
| quit(1) | |
| initDB(globalDB) | |
| let existingMessages = getMessages(globalDB) | |
| if existingMessages.len == 0: | |
| info "Inserting test data" | |
| insertMessage(globalDB, "Welcome to the server!") | |
| insertMessage(globalDB, "This is a test message") | |
| info "Test data inserted" | |
| info "Database initialized successfully" | |
| except DbError as e: | |
| error "Failed to initialize database: ", e.msg | |
| if globalDB != nil: | |
| closeDB(globalDB) | |
| globalDB = nil | |
| deinitLock(dbLock) | |
| quit(1) | |
| # Setup database before routes | |
| setupDatabase() | |
| settings: | |
| bindAddr = "0.0.0.0" | |
| reusePort = true | |
| numThreads = 1 # Explicitly enforce single-threaded mode | |
| routes: | |
| get "/": | |
| info "Handling GET /" | |
| resp """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Message Board</title> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <script src="https://unpkg.com/htmx.org@1.9.10"></script> | |
| </head> | |
| <body> | |
| <div class="container mt-4"> | |
| <h1 class="mb-4">Message Board</h1> | |
| <!-- Form for adding messages --> | |
| <div class="card mb-4"> | |
| <div class="card-body"> | |
| <h5 class="card-title">Add New Message</h5> | |
| <form hx-post="/add-message" hx-target="#message-table" hx-swap="innerHTML"> | |
| <div class="mb-3"> | |
| <label for="content" class="form-label">Message Content</label> | |
| <input type="text" class="form-control" id="content" name="content" placeholder="Enter your message" required> | |
| </div> | |
| <button type="submit" class="btn btn-primary">Add Message</button> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Table for displaying messages --> | |
| <div class="card"> | |
| <div class="card-body"> | |
| <h5 class="card-title">Messages</h5> | |
| <div id="message-table" hx-get="/message" hx-trigger="load"> | |
| <!-- Messages will be loaded here --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| get "/hello": | |
| info "Handling GET /hello" | |
| resp "Hello from HTMX!" | |
| get "/message": | |
| try: | |
| info "Handling GET /message" | |
| let messages = safeGetMessages() | |
| var html = """ | |
| <table class="table table-striped"> | |
| <thead> | |
| <tr> | |
| <th scope="col">ID</th> | |
| <th scope="col">Content</th> | |
| <th scope="col">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for msg in messages: | |
| html.add(""" | |
| <tr> | |
| <td>""" & $msg.id & """</td> | |
| <td>""" & msg.content & """</td> | |
| <td> | |
| <button class="btn btn-danger btn-sm" | |
| hx-delete="/delete-message/""" & $msg.id & """" | |
| hx-target="#message-table" | |
| hx-swap="innerHTML" | |
| onclick="return confirm('Are you sure you want to delete this message?')"> | |
| Delete | |
| </button> | |
| </td> | |
| </tr> | |
| """) | |
| html.add(""" | |
| </tbody> | |
| </table> | |
| """) | |
| resp html, "text/html" | |
| except DbError as e: | |
| error "Error retrieving messages: ", e.msg | |
| halt Http500, "Failed to retrieve messages" | |
| post "/add-message": | |
| try: | |
| info "Handling POST /add-message" | |
| let content = request.params.getOrDefault("content", "") | |
| if content.len > 0: | |
| safeInsertMessage(content) | |
| let messages = safeGetMessages() | |
| var html = """ | |
| <table class="table table-striped"> | |
| <thead> | |
| <tr> | |
| <th scope="col">ID</th> | |
| <th scope="col">Content</th> | |
| <th scope="col">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for msg in messages: | |
| html.add(""" | |
| <tr> | |
| <td>""" & $msg.id & """</td> | |
| <td>""" & msg.content & """</td> | |
| <td> | |
| <button class="btn btn-danger btn-sm" | |
| hx-delete="/delete-message/""" & $msg.id & """" | |
| hx-target="#message-table" | |
| hx-swap="innerHTML" | |
| onclick="return confirm('Are you sure you want to delete this message?')"> | |
| Delete | |
| </button> | |
| </td> | |
| </tr> | |
| """) | |
| html.add(""" | |
| </tbody> | |
| </table> | |
| """) | |
| resp html, "text/html" | |
| else: | |
| warn "Empty content in POST /add-message" | |
| halt Http400, "Message content cannot be empty" | |
| except DbError as e: | |
| error "Error adding message: ", e.msg | |
| halt Http500, "Failed to add message" | |
| delete "/delete-message/@id": | |
| try: | |
| info "Handling DELETE /delete-message/", @"id" | |
| let id = parseInt(@"id") | |
| safeDeleteMessage(id) | |
| let messages = safeGetMessages() | |
| var html = """ | |
| <table class="table table-striped"> | |
| <thead> | |
| <tr> | |
| <th scope="col">ID</th> | |
| <th scope="col">Content</th> | |
| <th scope="col">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| for msg in messages: | |
| html.add(""" | |
| <tr> | |
| <td>""" & $msg.id & """</td> | |
| <td>""" & msg.content & """</td> | |
| <td> | |
| <button class="btn btn-danger btn-sm" | |
| hx-delete="/delete-message/""" & $msg.id & """" | |
| hx-target="#message-table" | |
| hx-swap="innerHTML" | |
| onclick="return confirm('Are you sure you want to delete this message?')"> | |
| Delete | |
| </button> | |
| </td> | |
| </tr> | |
| """) | |
| html.add(""" | |
| </tbody> | |
| </table> | |
| """) | |
| resp html, "text/html" | |
| except DbError as e: | |
| error "Error deleting message: ", e.msg | |
| halt Http500, "Failed to delete message" | |
| except ValueError: | |
| warn "Invalid ID format in DELETE /delete-message/", @"id" | |
| halt Http400, "Invalid message ID" | |
| proc cleanup() {.noconv.} = | |
| info "Shutting down server" | |
| withLock dbLock: | |
| if globalDB != nil: | |
| closeDB(globalDB) | |
| globalDB = nil | |
| deinitLock(dbLock) | |
| info "Server shutdown complete" | |
| system.addQuitProc(cleanup) | |
| info "Server starting on port ", PORT | |
| info "Visit http://localhost:", PORT, " to test the application" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment