Writing Real-Time Voting App In Nim #1
Tags: webdev, tutorial, frontend, backend
Published: 2023-08-11 16:02:35

## Get Started
In this article we'll use these technologies:
- [Nim programming language](https://nim-lang.org) 🔨
- [HappyX web framework](http://hapticx.nim.town/) 🎴
- [Tailwind CSS 3](https://tailwindcss.com) ✨
- 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:
- [official site](https://nim-lang.org/install.html)
- [choosenim tool](https://github.com/dom96/choosenim)

In this article we'll use choosenim.

Enter this command and follow instructions
```bash
wget -qO - https://nim-lang.org/choosenim/init.sh | sh
```

Next, we choose Nim v2.0.0
```bash
choosenim 2.0.0
```

Now we need to install HappyX web framework. We can do it with `nimble` package manager:
```bash
nimble install happyx@#head
```

## Create Project
To create project just use these commands
```bash
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`:

```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!"
```

I create test data:
![poll titles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qnabi139hd02bibqz61b.png)
![poll answers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wius83c5qv761t6on5mi.png)


Now, let's write the frontend part. Go to 📁 `vote_app/client/` and enter command:
```bash
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:

```nim
# 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`:
```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)

![Frontend header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xi997z5c5nfxh3pohq2a.png)

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:
```nim
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:
```nim
# Declare Auth request model to user registration
model Auth:
  username: string
  password: string
```

Authorization mount 🔌
```nim
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 💡
```nim
# 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:
```bash
cd vote_app/server/src
nim c -r main.nim
```

| User registration [POST] | User authorization [GET] |
| :---: | :---: |
| ![User registration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ghsq6xuqgdaev91lutsc.png) | ![User authorization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mv2wztuk3rsz0axheoy9.png) |

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

```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`:
```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
![Authorization window](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i15346u1qkbuvnmexaqd.png)

Let's show polls on user authorized ✨

First we'll add `GET` method that will respond polls data on the server
```nim
  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:
```nim
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`:
```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:
![Polls](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j8d388xdyktykixkypee.png)

See you soon! 👋

[Source code](https://github.com/HapticX/happyx-tutorial-series/tree/b13cc1808e9ac79677560a56e53895291c7e49dc/vote_app)