Skip to content

Instantly share code, notes, and snippets.

@naranyala
Created August 1, 2025 02:25
Show Gist options
  • Select an option

  • Save naranyala/b169d8423c5b171648b50aa575b0527a to your computer and use it in GitHub Desktop.

Select an option

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
# 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