Как создать веб-приложение, если вы пишите на Nim? Что такое HappyX и как можно создать на нем приложение для заметок? Обо всем этом вы узнаете в полной статье.

Здравствуйте! Меня зовут Никита, и сегодня я хочу рассказать вам о создании веб-приложений на языке программирования Nim.
Коротко о Nim
Начнем с простого. Nim - это язык программирования, который может компилироваться в C, C++, Objective C и в JavaScript. Благодаря этому мы можем писать как серверные приложения, так и клиентские.
Если у вас не установлен Nim, установите его, следуя инструкции на официальном сайте.
Что такое HappyX?
HappyX - это полнофункциональный веб-фреймворк, написанный на этом самом Nim. И исходя из вышесказанного, он позволяет разрабатывать серверные и клиентские веб-приложения. Для серверной части он компилируется в C, а для клиентской в JavaScript.
Хорошо, а дальше что?
Серверная часть
Разрабатывать мы ее будем все на том же HappyX с использованием примитивной sqlite, представленной нам библиотекой Nim.
Сперва необходимо поставить библиотеки happyx и db_connector. Делается это с помощью пакетного менеджера nimble:
nimble install happyx@#head nimble install db_connector
После установки библиотек создаем проект с помощью happyx cli:
hpx create --name server --kind SSR --language Nim
Далее во время выполнения команды выбираем don't use templates и создаем клиентский проект:
hpx create --name client --kind SPA --language Nim -u
Теперь переходим в /server/src/main.nim и импортируем необходимые библиотеки:
import happyx, db_connector/db_sqlite
Затем создадим модели запроса для создания и редактирования наших заметок:
# Модель запроса # Ниже мы будем обрабатывать ее в виде JSON model CreateNote: # единственное обязательное поле string name: string model EditNote: completed: bool
Далее объявляем наше серверное приложение:
# Задаем хост и порт 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 ); """)
Пока что ничего сложного, верно?
Создадим POST запрос на создание заметки:
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}
И запускаем наше приложение:
nim c -r server/src/main
Пробуем сделать запрос:

POST запроса на создание заметкиВзглянем на нашу таблицу:

Отлично! Что дальше?
Теперь попробуем сделать GET запрос на получение всех заметок:
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 }}
Взглянем на Postman:

GET запроса на получение всех заметокНаконец, создадим PATCH запрос:
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"}
И снова смотрим Postman:

PATCH запроса
PATCH запросаКруто! Что теперь?
На этом разработка серверной части окончена. Теперь мы перейдем к написанию клиентской части.
Клиентская часть
Здесь мы переходим в client/src/main.nim и редактируем его. Для начала разберемся с импортом:
import happyx, std/strformat, # форматирование строк std/jsfetch, # fetch std/asyncjs, # async std/sugar, # ��интаксический сахар std/httpcore, # HTTP методы std/json # работа с JSON
Теперь зададим базовую ссылку на наше API:
# базовый URL для API const BASE = "http://localhost:5000"
И объявим тип заметки для дальнейшей обработки:
# тип для заметки type Note = object id: cint name: cstring completed: bool
После чего объявим реактивные переменные:
var # реактивный список заметок notes = remember newSeq[Note]() # реактивное название для новой заметки newName = remember ""
Теперь вернемся к серверной части и вспомним, что мы там писали. Создадим три процедуры для взаимодействия с 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}) ))
Давайте получим список заметок при загрузке страницы:
# Сразу получаем список заметок discard updateNotes()
А теперь время верстки!
# Объявляем наше одностраничное приложение в элементе с 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 await addNote(newName) discard await 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 await toggleNote(notes[i]) discard await updateNotes()
Наконец-то, теперь мы можем запустить?
Все верно! Самое время протестировать наше приложение! Запускаем бэкенд:
nim c -r server/src/main
И отдельно запускаем фронтенд:
cd client hpx dev --reload --port 8000

Вот и все! Мы написали простое веб-приложение, используя Nim и веб-фреймворк HappyX.
Полезные ссылки
Исходный код из данной статьи
Документация по Nim
Гайд по HappyX
