062 Обзор архитектуры
Предобработчиком нашего запроса будет middleware
, который уже будет передавать данные на обработку в контроллер.
Контроллер непосредственно обрабатывает запросы от пользователей.
С точки зрения слоёной архитекторы, вся бизнес-логика должна содержаться в сервисах. Сам контроллер должен просто вызвать сервис. Сервис уже сам общается с репозиторием.
Репозиторий отвечает за непосредственное общение с базой данных.
Эта архитектура позволяет легко заменять определённые технологии. Например, поменяем нашу базу данных - нам придётся поменять только логику репозитория.
Так же эту систему будет легко поддерживать, так как вся логика для отдельного модуля содержится в этом одном модуле. Если нужно будет поменять сервис, то нам нужно будет править только сервис.
063 Пишем класс приложения
Напишем скрипты для запуска нашего приложения
package.json
"main": "index.js",
"type": "commonjs",
"scripts": {
"start": "node ./dist/main.js",
"build": "tsc",
"test": ""
},
Далее настраиваем TS
tsconfig.json
"compilerOptions": {
"target": "es2022",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "./dist",
}
Тут мы инициализируем наше приложение и запускаем
main.ts
import { App } from "./app";
// фукнция запуска приложения
async function bootstrap() {
const app = new App();
await app.init();
}
bootstrap();
Тут мы будет располагаться класс нашего приложения, через который будет происходить запуск всех компонентов
app.ts
import express, { Express, Request, Response, NextFunction } from "express";
import { Server } from "http";
import { userRouter } from "./users/users";
// класс с нашим приложением
export class App {
app: Express;
server: Server;
port: number;
constructor() {
this.app = express();
this.port = 8000;
this.server = new Server();
}
// тут мы будем вдальнейшем определять подключенные роуты
useRoutes() {
this.app.use("/users", userRouter);
}
// тут будет происходить инициализация нашего приложения
public async init() {
this.useRoutes();
this.server = this.app.listen(this.port);
console.log(`Сервер запущен на http://localhost:${this.port}`);
}
}
064 Добавляем логгер
Добавлять логгер мы будем отдельным модулем, который будет внедряться путём Dependency Injection . То есть мы не будем инстанциировать наш логгер внутри App
или делать логгер статичным, чтобы его просто вызывать. Мы будем передавать инстанс логгера прямо при вызове нашего приложения
Установим сам логгер
npm i tslog
Тут реализован сам модуль логгера
logger.service.ts
// @ts-ignore
import { Logger } from "tslog";
export class LoggerService {
public logger: Logger<string>;
constructor() {
this.logger = new Logger({
// тут по идее должны были быть настройки, но нужно искать актуальную документацию
// displayInstanceName: false,
// displayLoggerName: false,
// displayFilePath: "hidden",
// displayFunctionName: false,
});
}
log(...args: unknown[]) {
this.logger.info(...args);
}
error(...args: unknown[]) {
// отправка в sentry // rollbar
this.logger.error(...args);
}
warn(...args: unknown[]) {
this.logger.warn(...args);
}
}
Тут мы подцепили логику выполнения логгера
app.ts
import express, { Express } from "express";
import { Server } from "http";
import { LoggerService } from "./logger/logger.service";
import { userRouter } from "./users/users";
export class App {
app: Express;
// @ts-ignore
server: Server;
port: number;
logger: LoggerService;
constructor(logger: LoggerService) {
this.app = express();
this.port = 8000;
this.logger = logger;
}
useRoutes() {
this.app.use("/users", userRouter);
}
public async init() {
this.useRoutes();
this.server = this.app.listen(this.port);
this.logger.log(`Сервер запущен на http://localhost:${this.port}`);
}
}
Конкретно мы внедряем через конструктор App зависимость от другого сервиса - это пример простого Dependency Injection
main.ts
import { App } from "./app";
import { LoggerService } from "./logger/logger.service";
async function bootstrap() {
const app = new App(new LoggerService());
await app.init();
}
bootstrap();
065 Базовый класс контроллера
Тип Pick<>
берёт из первого интерфейса (который был вложен первым аргументом) выделенные типы, которые в него были переданы (второй аргумент)
common > route.inteface.ts
import { Router, NextFunction, Request, Response } from "express";
export interface IControllerRoute {
// путь
path: string;
// колбэк-функция роута
func: (req: Request, res: Response, next: NextFunction) => void;
// и один из методов, по которым будет осуществляться обработка
method: keyof Pick<Router, "get" | "post" | "delete" | "patch" | "put">;
}
Для реализации базового класса контроллера мы можем воспользоваться абстракцией
common > base.controller.ts
import { Router, Response } from "express";
import { LoggerService } from "../logger/logger.service";
import { IControllerRoute } from "./route.inteface";
// это абстрактный базовый контроллер, который будет прототипом для остальных контроллеров
export abstract class BaseController {
private readonly _router: Router;
constructor(private logger: LoggerService) {
this._router = Router();
}
// тут мы получаем роутер
get router() {
return this._router;
}
// тут мы будем производитьотправку сообщения о статусе ответа
public send<T>(res: Response, code: number, message: T) {
res.type("application/json");
return res.status(code).json(message);
}
// это быстрый метод для отправки о том, что сообщение нормально прошло
public ok<T>(res: Response, message: T) {
return this.send<T>(res, 200, message);
}
// отправляем статус о том, что ответ реализуем
public created(res: Response) {
return res.sendStatus(201);
}
// тут мы уже будем связывать наши роуты
// получаем массив роутов, которые соответствуют интерфейсу
protected bindRoutes(routes: IControllerRoute[]) {
// перебираем роуты
for (const route of routes) {
// выведем метод роута (гет/пост/ и остальные) и путь
this.logger.log(`${route.method} ${route.path}`);
// и тут нам нужно связать контекст функции с контекстом контроллера (так как контекст сейчас относится к функции, которая находится внутри экспрешшена)
const handler = route.func.bind(this);
// по методу роута (5 штук) мы вызываем по пути определённую функцию
this.router[route.method](route.path, handler);
}
}
}
066 Упражнение - Контроллер пользователей
Тут мы реализовали сам контроллер пользователя, у которого осуществили привязку функций роутов к классу и реализовали две функции: вход и регистрация
users > user.controller.ts
import { NextFunction, Request, Response } from "express";
import { BaseController } from "../common/base.controller";
import { LoggerService } from "../logger/logger.service";
// Это контроллер на работу функционала пользователя
export class UserController extends BaseController {
constructor(logger: LoggerService) {
super(logger);
this.bindRoutes([
{
path: "/register",
method: "post",
func: this.register,
},
{
path: "/login",
method: "post",
func: this.login,
},
]);
}
// обработка логина пользователя
login(req: Request, res: Response, next: NextFunction) {
this.ok(res, "login");
}
// обработка регистрации пользователя
register(req: Request, res: Response, next: NextFunction) {
this.ok(res, "register");
}
}
И теперь в метод useRoutes
мы можем положить роутер контроллера пользователя
app.ts
import express, { Express } from "express";
import { Server } from "http";
import { LoggerService } from "./logger/logger.service";
import { UserController } from "./users/users.controller";
export class App {
app: Express;
// @ts-ignore
server: Server;
port: number;
logger: LoggerService;
userController: UserController;
constructor(logger: LoggerService, userController: UserController) {
this.app = express();
this.port = 8000;
this.logger = logger;
this.userController = userController;
}
useRoutes() {
this.app.use("/users", this.userController.router);
}
public async init() {
this.useRoutes();
this.server = this.app.listen(this.port);
this.logger.log(`Сервер запущен на http://localhost:${this.port}`);
}
}
Тут уже видна небольшая корявость в прокидывании зависимостей логгера, так как он постоянно переиспользуется и даже выведен в отдельную функцию, но с этим можно будет решить вопрос в будущем
main.ts
import { App } from "./app";
import { LoggerService } from "./logger/logger.service";
import { UserController } from "./users/users.controller";
async function bootstrap() {
const logger = new LoggerService();
const app = new App(logger, new UserController(logger));
await app.init();
}
bootstrap();
И теперь тут можно увидеть, что наш пользователь зарегистрирован и залогинен
067 Обработка ошибок
Это класс самой нашей сгенерированной ошибки. Он формирует нужный вид ошибки, которую в дальнейшем мы будем обрабатывать
errors > http-error.class.ts
export class HTTPError extends Error {
statusCode: number;
context?: string;
constructor(statusCode: number, message: string, context?: string) {
super(message);
this.statusCode = statusCode;
this.message = message;
this.context = context;
}
}
Тут у нас хранится интерфейс ошибки, которую мы словили
errors > exception.filter.interface.ts
import { NextFunction, Request, Response } from "express";
export interface IExceptionFilter {
catch: (err: Error, req: Request, res: Response, next: NextFunction) => void;
}
А тут уже реализуется сама обработка и вывод ошибки
errors > exception.filter.ts
import { NextFunction, Request, Response } from "express";
import { LoggerService } from "../logger/logger.service";
import { IExceptionFilter } from "./exception.filter.interface";
import { HTTPError } from "./http-error.class";
// Это класс фильтра ошибок
export class ExceptionFilter implements IExceptionFilter {
logger: LoggerService;
constructor(logger: LoggerService) {
this.logger = logger;
}
// этот метод отлавливает ошибки
catch(
err: Error | HTTPError,
req: Request,
res: Response,
next: NextFunction
) {
// тут уже мы в зависимости от инстанса ошибки можем определить логику поведения
if (err instanceof HTTPError) {
// выводим ошибку со всеми её данными http-ошибки
this.logger.error(
`[${err.context}] Ошибка ${err.statusCode}: ${err.message}`
);
// отправляем статус ошибки и сообщение
res.status(err.statusCode).send({ err: err.message });
} else {
// тут уже выводится просто ошибка
this.logger.error(`${err.message}`);
// отправляем обратно клиенту сообщение об ошибке - тут уже только 500ый код
res.status(500).send({ err: err.message });
}
}
}
Подключаем наш обработчик ошибок в приложение
app.ts
import express, { Express } from "express";
import { Server } from "http";
import { ExceptionFilter } from "./errors/exception.filter";
import { LoggerService } from "./logger/logger.service";
import { UserController } from "./users/users.controller";
export class App {
app: Express;
// @ts-ignore
server: Server;
port: number;
logger: LoggerService;
userController: UserController;
exceptionFilter: ExceptionFilter;
constructor(
logger: LoggerService,
userController: UserController,
exceptionFilter: ExceptionFilter
) {
this.app = express();
this.port = 8000;
this.logger = logger;
this.userController = userController;
this.exceptionFilter = exceptionFilter;
}
useRoutes() {
this.app.use("/users", this.userController.router);
}
// и тут мы накладываем обработчик на использование фильтра для вывода ошибок
useExceptionFilters() {
// тут так же нужно сделать привязку контекста вызова к инстансу фильтра ошибок данного класса
this.app.use(this.exceptionFilter.catch.bind(this.exceptionFilter));
}
// метод инициализации
public async init() {
this.useRoutes();
this.useExceptionFilters();
this.server = this.app.listen(this.port);
this.logger.log(`Сервер запущен на http://localhost:${this.port}`);
}
}
И тут нам нужно реализовать мини-DI , который выглядит немного коряво
main.ts
import { App } from "./app";
import { ExceptionFilter } from "./errors/exception.filter";
import { LoggerService } from "./logger/logger.service";
import { UserController } from "./users/users.controller";
async function bootstrap() {
// создаём единственный инстанс логгера
const logger = new LoggerService();
// вызываем наше приложение
const app = new App(
// это логгер, который используется в нашей системе
logger,
// это инстанс контроллера пользователя
new UserController(logger),
// добавляем сюда так же наш инстанс ошибки
new ExceptionFilter(logger)
);
// запускаем сервер
await app.init();
}
bootstrap();
И для примера вызовем ошибку отправки запроса при логине пользователя
users > user.controller.ts
// обработка логина пользователя
login(req: Request, res: Response, next: NextFunction) {
// и таким способом в любом месте контроллера можно вызвать ошибку
next(new HTTPError(401, "ошибка авторизации"));
// this.ok(res, "login");
}
Успешная ошибка!
Эта ошибка, которая пришла к нам на фронт
Это ошибка, которая пришла к нам в дебаггер