Developing a simple note taking app on HappyX

How to create a web application if you write in Nim? What is HappyX and how can you create a note-taking application with it? You will learn all about it in the full article.

Cover of the article

Article cover

Hello! My name is Nikita, and today I want to tell you about creating web applications in the Nim programming language.

Briefly about Nim

Let's start with something simple. Nim – is a programming language that can be compiled into C, C++, Objective C and JavaScript. Thanks to this, we can write both server and client applications.

If you don't have Nim installed, install it by following these steps: instructions on the official site.

What is HappyX?

HappyX is a full-featured web framework written in Nim itself. And based on the above, it allows you to develop server and client web applications. For the server side it is compiled in C, and for the client side in JavaScript.

Okay, so what next?

Server part

We will develop it on the same HappyX using the primitive sqlite presented to us library Nim.

First you need to install the libraries happyx And db_connector. This is done using a package manager. nimble:

nimble install happyx@#head
nimble install db_connector

After installing the libraries, create a project using happyx cli:

hpx create --name server --kind SSR --language Nim

Next, while executing the command, select don't use templates and create a client project:

hpx create --name client --kind SPA --language Nim -u

Now let's go to /server/src/main.nim and import the necessary libraries:

import
  happyx,
  db_connector/db_sqlite

Next, let's create query models for creation And editing our notes:

# Модель запроса
# Ниже мы будем обрабатывать ее в виде JSON
model CreateNote:
  # единственное обязательное поле string
  name: string

model EditNote:
  completed: bool

Next we declare our server application:

# Задаем хост и порт
serve "127.0.0.1", 5000:
  # Преднастройка сервера в gcsafe (garbage collector safe) области
  setup:
    # Подключаем базу данных
    var db = open("notes.db", "", "", "")

    # Создаем таблицу, если она не существует
    db.exec(sql"""
    CREATE TABLE IF NOT EXISTS notes(
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name VARCHAR(50) NOT NULL,
      completed INTEGER NOT NULL DEFAULT 0
    );
    """)

Nothing complicated yet, right?

Let's create POST request to create a note:

serve "127.0.0.1", 5000:
  setup:
    ...

  # Объявляем POST запрос для создания новой заметки
  post "/note[note:CreateNote]":
    # Выведем название заметки
    echo note.name
    # Вставляем заметку в базу данных и получаем ее ID
    let id = db.insertId(sql"INSERT INTO notes (name) VALUES (?)", note.name)
    # Возвращаем ID заметки в ответе
    return {"response": id}

And we launch our application:

nim c -r server/src/main

Let's try to make a request:

Result of POST request to create a note

Result POST request to create a note

Let's take a look at our table:

Contents of the notes table

Contents of the notes table

Great! What's next?

Now let's try to do GET request to get all notes:

  post "/note[note:Note]":
    ...

  # GET запрос для получения всех заметок
  get "/notes":
    # Список заметок:
    var notes = %*[]
    # Пробегаемся по всем строчкам:
    for row in db.rows(sql"SELECT * FROM notes"):
      # Добавляем новый элемент в список
      notes.add %*{"id": row[0].parseInt, "name": row[1], "completed": row[2] != "0"}
    return {"response": {
      "items": notes,
      "size": notes.len
    }}

Let's take a look at Postman:

Result of executing a GET request to get all notes

Execution result GET request for all notes

Finally, let's create PATCH request:

  get "/notes":
    ...

  # PATCH запрос на изменение заметки по ее ID
  patch "/note/{noteId:int}[note:EditNote]":
    # Смотрим, есть ли такая заметка вообще
    var row = db.getRow(sql"SELECT * FROM notes WHERE id = ?", noteId)
    # заметка не найдена - возвращаем ошибку
    if row[0] == "":
      statusCode = 404
      return {"error": "заметка с таким ID не найдена"}
    # Обновляем нашу заметку
    db.exec(sql"UPDATE notes SET completed = ? WHERE id = ?", note.completed, noteId)
    # И возвращаем успешный статус
    return {"response": "success"}

And let's look at Postman again:

Successful result of PATCH request execution

Successful execution result PATCH request

Invalid result of PATCH request

Incorrect execution result PATCH request

Cool! Now what?

This completes the development of the server part. Now we will move on to writing the client part.

Client side

Here we go to client/src/main.nim and edit it. First, let's look at importing:

import
  happyx,
  std/strformat,  # форматирование строк
  std/jsfetch,  # fetch
  std/asyncjs,  # async
  std/sugar,  # синтаксический сахар
  std/httpcore,  # HTTP методы
  std/json  # работа с JSON

Now let's set a basic link to our API:

# базовый URL для API
const BASE = "http://localhost:5000"

And we will declare the note type for further processing:

# тип для заметки
type Note = object
  id: cint
  name: cstring
  completed: bool

Then we declare reactive variables:

var
  # реактивный список заметок
  notes = remember newSeq[Note]()
  # реактивное название для новой заметки
  newName = remember ""

Now let's go back to the server part and remember what we wrote there. Let's create three procedures for interacting with the API:

proc updateNotes() {.async.} =
  # Делаем запрос к серверу на получение всех заметок
  await fetch(fmt"{BASE}/notes".cstring)
    # Получаем JSON
    .then((response: Response) => response.json())
    .then(proc(data: JsObject) =
      # Преобразуем JSON в список Note
      var tmpNotes: seq[Note] = collect:
        for i in data["response"]["items"]:
          i.to(Note)
      # Если размер списка не изменился - просто меняем параметры
      if notes.len == tmpNotes.len:
        for i in 0..<tmpNotes.len:
          notes[i] = tmpNotes[i]
      else:
        # Если размер списка изменился - полностью меняем список
        notes.set(tmpNotes)
    )


proc toggleNote(note: Note) {.async.} =
  # Отправляем PATCH запрос
  discard await fetch(fmt"{BASE}/note/{note.id}".cstring, newfetchOptions(
    HttpPatch, $(%*{"completed": not note.completed})
  ))


proc addNote(name: string) {.async.} =
  # Отправляем POST запрос
  discard await fetch(fmt"{BASE}/note".cstring, newfetchOptions(
    HttpPost, $(%*{"name": name})
  ))

Let's get a list of notes when the page loads:

# Сразу получаем список заметок
discard updateNotes()

Now it's time for layout!

# Объявляем наше одностраничное приложение в элементе с ID app
appRoutes "app":
  # Главный маршрут
  "/":
    tDiv(class = "flex flex-col gap-2 w-fit p-8"):
      tDiv(class = "flex"):
        # input для 
        tInput(id = "newNameChanger", class = "rounded-l-full px-6 py-2 border-2 outline-none", value = $newName):
          @input:
            # Меняем название заметки
            newName.set($ev.target.InputElement.value)
        tButton(class = "bg-green-400 hover:bg-green-500 active:bg-green-600 rounded-r-full px-4 py-2 transition-all duration-300"):
          "Добавить"
          @click:
            # Добавляем новую заметку
            discard addNote(newName).then(() => (discard updateNotes()))
            newName.set("")
      tDiv(class = "flex flex-col gap-2"):
        # Пробегаемся по заметкам
        for i in 0..<notes.len:
          tDiv(
            class =
              # Меняем класс в зависимости от выполненности заметки
              if notes[i].completed:
                "rounded-full select-none px-6 py-2 cursor-pointer hover:scale-110 translation-all duration-300 bg-green-300"
              else:
                "rounded-full select-none px-6 py-2 cursor-pointer hover:scale-110 translation-all duration-300 bg-red-300"
          ):
            # аналогично с эмоджи
            if notes[i].completed:
              "✅ "
            else:
              "❌ "
            {notes[i].name}
            @click:
              # При нажатии шлем PATCH запрос и обновляем список заметок
              discard toggleNote(notes[i]).then(() => (discard updateNotes()))

Finally, can we launch now?

That's right! It's time to test our application! Let's launch the backend:

nim c -r server/src/main

And we launch the frontend separately:

cd client
hpx dev --reload --port 8000
The final application with backend and frontend running

The final application with backend and frontend running

That's it! We've written a simple web application using Nim and the HappyX web framework.

useful links

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *