DEV Community

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

Posted on • Updated 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