IM на основе NodeJS Express и Angular 7. Часть 3

Часть 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.