DEV Community

Cover image for Writing Real-Time Voting App In Nim #1
Ethosa
Ethosa

Posted on • Edited on

Writing Real-Time Voting App In Nim #1

Get Started

In this article we'll use these technologies:

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 
Enter fullscreen mode Exit fullscreen mode

Next, we choose Nim v2.0.0

choosenim 2.0.0 
Enter fullscreen mode Exit fullscreen mode

Now we need to install HappyX web framework. We can do it with nimble package manager:

nimble install happyx@#head 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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!" 
Enter fullscreen mode Exit fullscreen mode

I create test data:
poll titles
poll answers

Now, let's write the frontend part. Go to 📁 vote_app/client/ and enter command:

hpx dev --reload 
Enter fullscreen mode Exit fullscreen mode

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 ❎" 
Enter fullscreen mode Exit fullscreen mode

📂 vote_app/client/src/main.nim:

# Import HappyX import happyx, components/[header] # Declare application with ID "app" appRoutes("app"): "/": # Component usage component Header 
Enter fullscreen mode Exit fullscreen mode

So we got this

❗ (webpage is 200% scaled)

Frontend header

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(@[]) 
Enter fullscreen mode Exit fullscreen mode

Next step is to declare the request model for POST method:

# Declare Auth request model to user registration model Auth: username: string password: string 
Enter fullscreen mode Exit fullscreen mode

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"} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Let's start our app:

cd vote_app/server/src nim c -r main.nim 
Enter fullscreen mode Exit fullscreen mode
User registration [POST] User authorization [GET]
User registration User authorization

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) ) ) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

So we got this authorization window
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 } 
Enter fullscreen mode Exit fullscreen mode

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) )) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

And we got this after authorization:
Polls

See you soon! 👋

Source code

Top comments (1)

Collapse
 
entykey profile image
Nguyen Huu Anh Tuan

very informative, thanks for the detailed post. <3