Часть 1. Серверная
Начнем с модели данных нашего приложения, которая может быть представлена следующей схемой:

Где мы имеем 4 таблицы с категориями и подкатегориями, товарами и изображениями. Все таблицы связаны друг с другом внешними ключами.
Наша задача заключается в том, чтобы создать RESTfull API сервис (серверная часть), используя NodeJS для возможности вывода каталога товаров в одностраничное приложение на Angular (клиентская часть).
Будем использовать СУБД PoptgresSQL.
Начнем с серверной части.
RESTfull API cервер на NodeJS и Express
Инструментарий
Прежде всего настроим наше рабочее окружение и сделаем максимально удобным процесс разработки и отладки.
Для установки NodeJS и приведения его в актуальное состояние в OC на основе Debian выполним следующую последовательность команд:
sudo apt install nodejssudo npm cache clean -fsudo npm install -g nsudo n stable
Проверяем установленную версию.
node -v
В данный момент у меня установлена версия 10.15.3.
Создадим каталог с будущим сервером и инициализируем в нем пакет npm (package.json).
mkdir servercd servernpm init
Далее, устанавливаем nodemon для того, чтобы сервер автоматически перезагружался при правках в коде, что сделает работу более комфортной.
npm install --save nodemon
Так как мы будем использовать Typescript, установим его.
npm install --save-dev typescript
Установим остальные инструменты, которые будем использовать в работе.
npm install express --savenpm install --save-dev @types/node ts-node
express – фреймворк для построения REST API
@types/node – плагин для разрешения типов для nodejs
ts-node – позволит запускать typescript файлы без транспиляции их в нативный javascript
Также установим типы и для express.
npm install @types/express --save
Создаем файл tsconfig.json c настройками транспилятора typescript.
{ "compilerOptions": { "target": "es6", "module": "commonjs", "outDir": "dist", "sourceMap": true }, "include": [ "src/**/*.ts" ], "exclude": [ "node_modules", ".vscode" ]}
Так как мы указали папку src в качестве исходной, добавим туда файл main.ts с кодом простейшего сервера.
import * as express from "express";const app = express();app.get("/", (req, res) => { res.send("Hello World")})const PORT = process.env.PORT || 3000;app.listen(PORT, () => { console.log(`Server is running in http://localhost:${PORT}`)})
Пропишем команды для запуска сервера в секции script файла package.json.
"scripts": { "start": "node --inspect=5858 -r ts-node/register ./src/main.ts", "start:watch": "nodemon", "build": "tsc"},
Разберем команду start, которая запускает сервер.
— inspect=5858 – включает порт 5858 для дебагинга проекта;
-r ts-node/register ./src/server.ts – запускает typescript файл где ts-node/register позволяет его транспилировать на лету.
“start”: “nodemon” – стартует процесс nodemon, отслеживающий изменения кода.
Осталось добавить настройки для nodemon в секцию nodemonConfig файла package.json.
"nodemonConfig": { "ignore": [ "**/*.test.ts", "**/*.spec.ts", ".git", "node_modules" ], "watch": [ "src" ], "exec": "npm start", "ext": "ts" }
Теперь можно запустить сервер командой.
npm start:watch
Для того, чтобы включить режим дебага в редакторе VS Code необходимо создать файл launch.json в каталоге .vscode со следующим содержимым где добавить процесс nodejs в редактор.
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Node: Nodemon", "processId": "${command:PickProcess}", "restart": true, "protocol": "inspector" } ]}
Теперь при нажатии значка дебага в левом верхнем углу и выбора соответствующего пункта в списке процессов автоматически начнется отладка и при установке брекпоинта в коде вас автоматом перебросит в редактор.

Вы также можете отлаживать код прямо в браузере chrome. Для этого введите в адресную строку chrome://inspect и сконфигурируйте настройки искомых сетевых устройств, добавив туда localhost:5858.

После чего ваш сервер станет доступен в списке Remote target и вы можете запустить отладку, нажав на ссылку Inspect.

Работа с базой данных PostgreSQL.
Теперь, когда мы настроили рабочее окружение, можно приступить к разработке REST API сервиса. В начале, соединимся с базой данных и получим записи из таблицы категорий нашего магазина штор.

Для работы с базой данных мы будем использовать пакет node-postgres, который установим командой
npm install --save pg
Далее, создадим соединение передав его параметры объекту Pool. Если их не передавать, он попытается их взять из переменных окружения, например PGHOST, PGDATABASE и т.д. что рекомендуется делать дабы избежать хардкода в проекте и случайно не выложить ваши секреты в открытый доступ.
const Pool = require('pg').Poolconst 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 и подключить его методы следующим образом.
import { CategoryAPI } from './api/category';const category_api = new CategoryAPI();app.get("/category/all", category_api.getAll);app.get("/category/one/:id", category_api.getOne);app.delete("/category/delete/:id", category_api.delete);app.post("/category/edit", category_api.post);app.put("/category/create", category_api.put);
Приведу пример вынесенного метода получения категорий (файл 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 – простой фреймворк для написания логики тестов, который их поочередно запускает и генерирует отчет.
Устанавливаем его глобально или локально командами:
npm install jasmine-core jasmine @types/jasmine jasmine-ts --save-dev
Request – библиотека для тестирования HTTP запросов.
npm install --save-dev request @types/request
После установки инициируем папку spec c настройками jasmine командой
npm test init
Изменим расширение файлов-тестов c js на ts в spec/support/jasmine.json
{ "spec_dir": "spec", "spec_files": [ "**/*[sS]pec.ts" ], "helpers": [ "helpers/**/*.ts" ], "stopSpecOnExpectationFailure": false, "random": true}
Добавим в tsconfig.json
"allowSyntheticDefaultImports": true
Для того чтобы иметь возможность импортировать библиотеку jasmine конструкцией
import jasmine from 'jasmine';
Добавим команду test в секцию scripts файла package.json
"scripts": { ... "test": "./node_modules/.bin/jasmine-ts"},
Пишем первый тест в файле spec/category.spec.ts на проверку статуса ответа сервера на 2 url-а.
import jasmine from 'jasmine';
import * as request from "request";
var base_url = "http://localhost:3000/"
describe("Category API test", function() {
it("All categories code 200", function(done) {
request.get(base_url+'category/all', function(error, response, body) {
expect(response.statusCode).toBe(200);
done();
});
});
it("Add category code 200", function(done) {
request.post(base_url+'category/create',{}, function(error, response, body) {
expect(response.statusCode).toBe(200);
done();
});
});
});
При успешном прохождении тестов командой npm run test мы должны видеть:
2 specs, 0 failures
Finished in 0.075 seconds
При не успешном.

Пример проверки post запроса на создание категории и передачи данных в формате json.
it("Add category OK", function(done) { let cat: any = {'name': 'Test category', 'name_slug': 'test-category' } request.post({ url: base_url+'category/create', body: cat, son: true }, function(error, response, body) { expect(JSON.parse(body)).toEqual({status: 0, message: 'Ok'}); done(); });});
Сама функция создания категории, удовлетворяющая условию теста и возвращающая 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 post(req: Request, res: Response){ const sql = 'INSERT INTO shop_subcategory(name, name_slug) VALUES($1, $2) RETURNING *' const values = [req.body.name, req.body.name_slug] try { const result = await pool.query(sql, values) res.status(200).json({status: 0, message: 'Ok'}) } catch (err) { console.log(err.stack) res.status(200).json({status: 1, message: 'Error!'}) }}
Так как мы использовали стиль async/away, мы должны заключать запрос к базе данных внутрь конструкции try/catch.
Особое внимание следует уделить формированию json ответа, описывающее товар. Проблема заключается в том, что формат описания товара может со временем меняться. К нему могут добавляться новые поля и массивы, рейтинги, комментарии и пр. Поэтому процесс формирования json (серелизация) информации о товаре удобней всего хранить в каком-то одном месте, которое задействуется при создании объекта товара. Если мы создадим класс товара, то конструктор в данном случае не подойдет, т.к. в нем мы будем использовать асинхронные операции await/async и конструктор не может возвратить промис. Выход из этой ситуации видется автору в создании статичного фабричного async метода класса товара, котрый и будет выполнять все необходимые запросы к базе данных, заполняя информацию о товаре.
Вот как может выглядеть такой класс:
class Good{ id: number; name: string; name_slug: string; desc: string; subcategory_id: number; subcategory: any; image: any; constructor(){} 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.