Начнем с модели данных нашего приложения, которая может быть представлена следующей схемой:
Где мы имеем 4 таблицы с категориями и подкатегориями, товарами и изображениями. Все таблицы связаны друг с другом внешними ключами.
Наша задача заключается в том, чтобы создать RESTfull API сервис (серверная часть), используя NodeJS для возможности вывода каталога товаров в одностраничное приложение на Angular (клиентская часть).
Будем использовать СУБД PoptgresSQL.
Начнем с серверной части.
RESTfull API cервер на NodeJS и Express
Инструментарий
Прежде всего настроим наше рабочее окружение и сделаем максимально удобным процесс разработки и отладки.
Для установки NodeJS и приведения его в актуальное состояние в OC на основе Debian выполним следующую последовательность команд:
sudo apt install nodejs sudo npm cache clean -f sudo npm install -g n sudo n stable
Проверяем установленную версию.
node -v
В данный момент у меня установлена версия 10.15.3.
Создадим каталог с будущим сервером и инициализируем в нем пакет npm (package.json).
mkdir server cd server npm init
Далее, устанавливаем nodemon для того, чтобы сервер автоматически перезагружался при правках в коде, что сделает работу более комфортной.
npm install --save nodemon
Так как мы будем использовать Typescript, установим его.
npm install --save-dev typescript
Установим остальные инструменты, которые будем использовать в работе.
Для того, чтобы включить режим дебага в редакторе VS Code необходимо создать файл launch.json в каталоге .vscode со следующим содержимым где добавить процесс nodejs в редактор.
Теперь при нажатии значка дебага в левом верхнем углу и выбора соответствующего пункта в списке процессов автоматически начнется отладка и при установке брекпоинта в коде вас автоматом перебросит в редактор.
Вы также можете отлаживать код прямо в браузере chrome. Для этого введите в адресную строку chrome://inspect и сконфигурируйте настройки искомых сетевых устройств, добавив туда localhost:5858.
После чего ваш сервер станет доступен в списке Remote target и вы можете запустить отладку, нажав на ссылку Inspect.
Работа с базой данных PostgreSQL.
Теперь, когда мы настроили рабочее окружение, можно приступить к разработке REST API сервиса. В начале, соединимся с базой данных и получим записи из таблицы категорий нашего магазина штор.
Для работы с базой данных мы будем использовать пакет node-postgres, который установим командой
npm install --save pg
Далее, создадим соединение передав его параметры объекту Pool. Если их не передавать, он попытается их взять из переменных окружения, например PGHOST, PGDATABASE и т.д. что рекомендуется делать дабы избежать хардкода в проекте и случайно не выложить ваши секреты в открытый доступ.
const Pool = require('pg').Pool const pool = new Pool({ user: 'postgres', host: 'localhost', database: 'curtains', password: '****', port: 5432, })
Далее мы импортируем нужные типы express и задействуем объект pool для выборки данных в главном роутинге.
import {Request, Response} from "express"; ... app.get("/", (req: Request, res: Response) => { pool.query('SELECT * FROM shop_category ORDER BY id ASC', (error, results) => { if (error) { throw error } res.status(200).json(results.rows) }) })
В результате на главной странице мы получим следующую картину:
Этот запрос можно переписать более короче (без колбеков), используя await/async.
app.get("/", async (req: Request, res: Response) => { const result = await pool.query('SELECT * FROM shop_category ORDER BY id ASC') res.status(200).json(result.rows) })
Добавим подкатегории в вывод, сделав в цикле по одному запросу на каждую категорию и получив ее подкатегории.
app.get("/", async (req: Request, res: Response) => { const result = await pool.query('SELECT * FROM shop_category ORDER BY id ASC') for(let category of result.rows){ const subcats = await pool.query(`SELECT * FROM shop_subcategory where category_id=${category.id}`) category.subcategories = subcats.rows; } res.status(200).json(result.rows) })
Так как держать всю логику приложения в одном файле main.ts не совсем правильно и удобно, вынесем API, связанное с категориями в отдельный файл api/category.ts.
Так будет выглядеть заготовка для будущего класса RESTfull API сервиса:
import {Request, Response} from "express";
export class CategoryAPI {
put(req: Request, res: Response){}
getAll(req: Request, res: Response){}
getOne(req: Request, res: Response){}
post(req: Request, res: Response){}
delete(req: Request, res: Response){} }
Теперь можно резко сократить код в main.ts, вынести всю логику работы с базой данных в класс CategoryAPI и подключить его методы следующим образом.
Приведу пример вынесенного метода получения категорий (файл api/category.ts)
export class CategoryAPI { put(req: Request, res: Response){}
async getAll(req: Request, res: Response){ const result = await pool.query('SELECT * FROM shop_category ORDER BY id ASC') for(let category of result.rows){ console.log(category); const subcats = await pool.query(`SELECT * FROM shop_subcategory where category_id=${category.id}`) category.subcategories = subcats.rows; } res.status(200).json(result.rows) }
getOne(req: Request, res: Response){}
post(req: Request, res: Response){}
delete(req: Request, res: Response){} }
Тестирование.
Теперь разберемся с тестированием нашего приложения, как неотъемлемой части любого приложения средней и высокой сложности. Под тестированием в данном случае будем понимать генерацию ряда HTTP запросов на url-ы сервера разными методами (GET POST и т.д.) и проверку результатов их выполнения. Эти тесты запускаются из консоли и выдают результат туда же. Для создания тестов будем использовать 3 библиотеки.
Jasmine – простой фреймворк для написания логики тестов, который их поочередно запускает и генерирует отчет.
Устанавливаем его глобально или локально командами:
Сама функция создания категории, удовлетворяющая условию теста и возвращающая json объект {status: 0, message: 'Ok'}.
Однако, для того, чтобы иметь возможность получить из тела запроса данные, необходимо подключить и задействовать библиотеку body-parser в приложении express.
Установка.
npm install body-parser @types/body-parser --save
Приминение.
const app = express(); import * as bodyParser from 'body-parser'; app.use(bodyParser.json());
Так как мы использовали стиль async/away, мы должны заключать запрос к базе данных внутрь конструкции try/catch.
Особое внимание следует уделить формированию json ответа, описывающее товар. Проблема заключается в том, что формат описания товара может со временем меняться. К нему могут добавляться новые поля и массивы, рейтинги, комментарии и пр. Поэтому процесс формирования json (серелизация) информации о товаре удобней всего хранить в каком-то одном месте, которое задействуется при создании объекта товара. Если мы создадим класс товара, то конструктор в данном случае не подойдет, т.к. в нем мы будем использовать асинхронные операции await/async и конструктор не может возвратить промис. Выход из этой ситуации видется автору в создании статичного фабричного async метода класса товара, котрый и будет выполнять все необходимые запросы к базе данных, заполняя информацию о товаре.
public static async serialize(json_obj: any){ let obj = new Good(); obj.id = json_obj.id; obj.name = json_obj.name; obj.name_slug = json_obj.name_slug; obj.desc = json_obj.desc; obj.subcategory_id = json_obj.subcategory_id; /// заполняем подкатегорию let sql_sub = 'SELECT * FROM shop_subcategory where id=$1'; let result_sub = await pool.query(sql_sub,[obj.subcategory_id]); obj.subcategory = {'name': result_sub.rows[0].name, 'name_slug': result_sub.rows[0].name_slug}; /// заполняем изображение let sql_image = 'SELECT * FROM shop_image where good_id=$1'; let result_image = await pool.query(sql_image,[obj.id]); obj.image = {'image': result_image.rows[0].image}; return obj; }
}
Теперь при формировании ответа сервером, например при запросе детальной информации о товаре, нам достаточно вызвать статический метод и он создаст заполненный объект, который мы и вернем браузеру в виде json-а.
export class GoodAPI {
async getOne(req: Request, res: Response){ const sql = 'SELECT * FROM shop_good where id=$1'; const result = await pool.query(sql,[req.params.id]); let good = await Good.serialize(result.rows[0]); res.json(good); } }
Выводы
В данной статье рассмотрен процесс создания рабочего окружения для разработки серверной части приложения на базе NodeJS, описана типовая структура проекта, удовлетворяющая требованиям RESTfull API архитектуры. Освещены приемы работы с базой данных PostgreSQL и механизмы осуществления SQL запросов библиотекой node-postgres. Также была затронута тема unit тестирования с использованием библиотеки Jasmine.
У вас остались вопросы?
Оставьте ваши контактные данные. Наш менеджер свяжется и проконсультирует вас.
беседу!