Get Started
In this article we'll use these technologies:
- Nim programming language 🔨
- HappyX web framework 🎴
- Tailwind CSS 3 ✨
- SQLite as database ⚙
To make real-time voting application we need websockets. Websockets is mostly useful to give real-time
features to your program.
HappyX web framework provides working with websockets so we need install only Nim and HappyX. 💡
Install
You can install Nim with two ways:
In this article we'll use choosenim.
Enter this command and follow instructions
wget -qO - https://nim-lang.org/choosenim/init.sh | sh
Next, we choose Nim v2.0.0
choosenim 2.0.0
Now we need to install HappyX web framework. We can do it with nimble
package manager:
nimble install happyx@#head
Create Project
To create project just use these commands
mkdir vote_app cd vote_app hpx create --kind:SPA --name:client --use-tailwind hpx create --kind:SSR --name:server
Next, open 📁 vote_app/server/src/main.nim
. Let's write some procedures to work with database. Final version of main.nim
:
# Import HappyX import happyx, # happyx web framework db_sqlite # stdlib Sqlite proc initDataBase(): DbConn = ## Creates Database connection. let res = open("votes.db", "", "", "") # Create users table res.exec(sql"""CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT NOT NULL, pswd TEXT NOT NULL );""") # Create votes table res.exec(sql"""CREATE TABLE IF NOT EXISTS vote ( id INTEGER PRIMARY KEY AUTOINCREMENT, userId INTEGER NOT NULL, pollId INTEGER NOT NULL, answerId INTEGER NOT NULL );""") # Create poll table res.exec(sql"""CREATE TABLE IF NOT EXISTS poll ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT NOT NULL );""") # Create poll answer table res.exec(sql"""CREATE TABLE IF NOT EXISTS answer ( id INTEGER PRIMARY KEY AUTOINCREMENT, pollId INTEGER NOT NULL, title TEXT NOT NULL );""") res proc users(db: DbConn): seq[Row] = ## Retrieves all created users db.getAllRows(sql"SELECT * FROM user") proc votes(db: DbConn): seq[Row] = ## Retrieves all created votes db.getAllRows(sql"SELECT * FROM vote") proc answers(db: DbConn): seq[Row] = ## Retrieves all created answers db.getAllRows(sql"SELECT * FROM answer") proc polls(db: DbConn): seq[Row] = ## Retrieves all created polls db.getAllRows(sql"SELECT * FROM poll") # Serve at http://127.0.0.1:5123 serve "127.0.0.1", 5123: # Connect to Database let db = initDataBase() # on GET HTTP method at http://127.0.0.1:5123/ get "/": # Respond plain text "Hello, world!"
Now, let's write the frontend part. Go to 📁 vote_app/client/
and enter command:
hpx dev --reload
This command executes our single-page application and automatically opens the browser. Flag --reload
enables hot code reloading 🔥.
Next, let's rewrite HelloWorld
component. Open 📂 vote_app/client/src/components/hello_world.nim
. Rename it to header.nim
. Final version of the file:
# Import HappyX import happyx # Declare component component Header: # Declare HTML template `template`: tDiv(class = "flex items-center justify-center w-fill sticky top-0 font-mono text-sm font-bold px-8 py-2 bg-purple-200"): tP(class = "scale-75 select-none cursor-pointer"): "✅ real-time voting app ❎"
📂 vote_app/client/src/main.nim
:
# Import HappyX import happyx, components/[header] # Declare application with ID "app" appRoutes("app"): "/": # Component usage component Header
So we got this
❗ (webpage is
200%
scaled)
Next step is authorization.
Authorization
Users must be authorized to vote. So first we will add POST
method to sing up and GET
method for login
Backend
Go to 📂 vote_app/server/src/main.nim
and write additional procedures:
proc userExists(db: DbConn, username: string): bool = ## Returns true if user is exists for user in db.users(): if user[1] == username: return true false proc getUser(db: DbConn, login, password: string): Row = ## get user by password and login for user in db.users(): # Compare user login and user password hash if user[1] == login and check_password(password, user[2]): return user Row(@[])
Next step is to declare the request model for POST
method:
# Declare Auth request model to user registration model Auth: username: string password: string
Authorization mount 🔌
mount Authorization: get "/sign-in[auth:Auth]": ## Authorizes user if available. ## ## On incorrect data responds 404. ## On success returns user's ID var user = db.getUser(query~username, query~password) if user.len == 0: statusCode = 404 return {"response": "failure"} else: return {"response": parseInt(user[0])} post "/sign-up[auth:Auth]": ## Registers user if available. ## ## When username is exists responds 404 if db.userExists(auth.username): statusCode = 404 return {"response": fmt"failure. user {auth.username} is exists."} else: db.exec( sql"INSERT INTO user (login, pswd) VALUES (?, ?)", auth.username, generate_password(auth.password) ) return {"response": "success"}
The final step is to use mount in our server 💡
# Setup CORS regCORS: origins: ["*"] headers: ["*"] methods: ["*"] credentials: true # Serve at http://127.0.0.1:5123 serve "127.0.0.1", 5123: # Connect to Database let db = initDataBase() # on GET HTTP method at http://127.0.0.1:5123/auth/... mount "/auth" -> Authorization
Let's start our app:
cd vote_app/server/src nim c -r main.nim
User registration [POST] | User authorization [GET] |
---|---|
![]() | ![]() |
Frontend
On the frontend side we will create a small reg/auth form. Let's create a file vote_app/client/src/components/auth.nim
:
import happyx component Authorization: # Callback that can be notified on auth is success 🔔 callback: (proc(authorized: bool): void) = (proc(authorized: bool) = discard) `template`: tDiv(class = "flex flex-col px-6 py-4 items-center rounded-md drop-shadow-2xl bg-purple-100 gap-8"): tP(class = "font-mono font-bold"): "authorization 🔐" tDiv(class = "flex flex-col gap-2"): tInput(id = "login", placeholder = "Username ...", class = "text-center rounded-md px-4 font-mono") tInput(id = "password", `type` = "password", placeholder = "Password ...", class = "font-mono text-center rounded-md px-4") tDiv(class = "flex justify-center items-center w-full justify-around"): tButton(class = "bg-none rounded-md px-2 text-sm font-mono border-2 border-purple-600 hover:border-purple-700 active:border-purple-800 transition-all"): "sign in" @click: # Try to authorize let inpLogin = document.getElementById("login") inpPassword = document.getElementById("password") auth(self.Authorization, inpLogin.value, inpPassword.value) [methods]: proc auth(username, password: cstring) = buildJs: function handleResponse(response): # Handle authorization response console.log(response) console.log(response.response) if typeof response.response == "number": nim: self.callback.val()(true) else: nim: self.callback.val()(false) fetch("http://localhost:5123/auth/sign-in?username=" + ~username + "&password=" + ~password).then( (e) => e.json().then( (response) => handleResponse(response) ) )
Let's use our component ✨
vote_app/client/src/main.nim
:
# Import HappyX import happyx, components/[header, auth] # Declare application with ID "app" appRoutes("app"): "/": # Component usage component Header tDiv(class = "absolute top-0 bottom-0 left-0 right-0 flex flex-col justify-center items-center"): component Authorization
So we got this authorization window
Let's show polls on user authorized ✨
First we'll add GET
method that will respond polls data on the server
get "/polls": var polls = newJArray() for poll in db.polls(): polls.add(%*{ "id": poll[0], "title": poll[1], "description": poll[2] }) return { "response": polls }
Create polls.nim
file into 📁 vote_app/client/src/components
.
Add the code snippet below:
import happyx component Polls: data: seq[tuple[ i: int, t, d: cstring, answers: seq[tuple[i, pId: int, t: cstring]] ]] = @[] `template`: tDiv(class = "flex flex-col gap-4 w-full h-full justify-center items-center px-8"): for poll in self.data: tDiv(class = "w-full rounded-md bg-purple-100 drop-shadow-xl px-8 py-2"): tP(class = "font-mono font-bold lowercase"): {poll.t} if poll.d.len > 0: tP(class = "font-mono text-sm opacity-75 lowercase"): {poll.d} tDiv(class = "flex flex-col gap-2"): for answer in poll.answers: tDiv(class = "flex font-mono lowercase font-sm justify-center items-center rounded-md bg-purple-50 select-none cursor-pointer"): {answer.t} @created: self.loadPolls() [methods]: proc loadPolls() = # Disable renderer at few time enableRouting = false # Write pure JavaScript with Nim syntax buildJs: function foreach(data): # Declare nim variables nim: var id: int title: cstring description: cstring answers: seq[tuple[i, pId: int, t: cstring]] = @[] # Load data from JS ~id = data.id ~title = data.title ~description = data.description # Load answers from JS And add in Nim for answer in data.answers: nim: var answerId: int pollId: int answerTitle: cstring ~answerId = answer.id ~pollId = answer.pollId ~answerTitle = answer.title nim: # Load JS data to Nim answers.add((answerId, pollId, answerTitle)) nim: self.data->add((id, title, description, answers)) function handlePolls(response): # Handle response response.forEach(e => foreach(e)) nim: enableRouting = true # Rerender our app application.router() # Fetch data from our API fetch("http://localhost:5123/polls").then(e => e.json().then( x => handlePolls(x.response) ))
Next step is component usage. Let's rewrite vote_app/client/src/main.nim
:
# Import HappyX import happyx, components/[header, auth, polls] var authState = remember false proc handleAuth(authorized: bool) = authState.set(authorized) # Declare application with ID "app" appRoutes("app"): "/": # Component usage component Header tDiv(class = "absolute top-0 bottom-0 left-0 right-0 flex flex-col justify-center items-center"): if not authState: component Authorization(callback = handleAuth) else: component Polls
And we got this after authorization:
See you soon! 👋
Top comments (1)
very informative, thanks for the detailed post. <3