Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ca63dde
feat: Установит `express` и типы
doosmurano Apr 20, 2026
645f396
feat: Добавит создание и запуск сервера `express`
doosmurano Apr 20, 2026
7255004
feat: Добавит интерфейс `Route`
doosmurano Apr 20, 2026
ca5b4fe
feat: Добавит интерфейс `Controller`
doosmurano Apr 20, 2026
72ae128
feat: Установит пакет `http-status-codes`
doosmurano Apr 20, 2026
5807b2c
feat: Добавит абстрактный класс `BaseController`
doosmurano Apr 22, 2026
92b5e80
feat: Установит `class-transformer`
doosmurano Apr 22, 2026
d773a3a
feat: Добавит интерфейс `ExceptionFilter`
doosmurano Apr 22, 2026
cb4271e
feat: Добавит класс `AppExceptionFilter`
doosmurano Apr 22, 2026
3f47f7d
feat: Подключит `AppExceptionFilter` в `RestApplication`
doosmurano Apr 22, 2026
a49241f
feat: Добавит контроллер `UserController`. WIP
doosmurano Apr 22, 2026
518cc9a
feat: Добавит метод `_initMiddleware` в `RestApplication`
doosmurano Apr 22, 2026
aca54f1
feat: Добавит пример ошибки в асинхронном обработчике. WIP
doosmurano Apr 22, 2026
e9bf6d0
fest: Обернёт ошибку в `next`. WIP
doosmurano Apr 22, 2026
92d2b2c
feat: Установит пакет `express-async-handler`
doosmurano Apr 22, 2026
ead2b74
feat: Добавит `asyncHandler` в `BaseController`
doosmurano Apr 22, 2026
8355839
feat: Проверит работу `asyncHandler`
doosmurano Apr 22, 2026
9c1467f
feat: Добавит ошибку `HttpError`
doosmurano Apr 28, 2026
9e4e04b
feat: Добавит функцию `createErrorObject`
doosmurano Apr 28, 2026
0f2ae76
feat: Добавим обработку `HttpError` в `AppExceptionFilter`
doosmurano Apr 28, 2026
61cfb9f
feat: Добавит создание пользователей в UserController
doosmurano May 12, 2026
24dcdb8
feat: Добавит метод login в UserController
doosmurano May 12, 2026
b1a8a77
feat: Добавит поле id в UserRdo
doosmurano May 19, 2026
71bfbd8
feat: Добавит маршрут проверки авторизации
doosmurano May 23, 2026
eec85cb
feat: Добавит маршрут logout
doosmurano May 23, 2026
37528bf
feat: Добавит маршрут uploadAvatar
doosmurano May 23, 2026
4ba9ca4
feat: Добавит контроллер 'OfferController' WIP
doosmurano May 23, 2026
ef8ceb3
feat: Добавит получение предложения по id в OfferController WIP
doosmurano May 23, 2026
b1d549d
feat: Добавит создание предложений в OfferController
doosmurano Jun 1, 2026
162bd93
feat: Исправит OfferRdo для location и userId
doosmurano Jun 1, 2026
690878d
feat: Добавит маршрут обновления предложения в OfferController
doosmurano Jun 1, 2026
770ad55
feat: Добавит маршрут удаления предложения в OfferController
doosmurano Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,482 changes: 1,000 additions & 482 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"devDependencies": {
"@types/convict": "6.1.6",
"@types/convict-format-with-validator": "6.0.6",
"@types/express": "5.0.6",
"@types/node": "18.17.17",
"@typescript-eslint/eslint-plugin": "6.7.0",
"@typescript-eslint/parser": "6.7.0",
Expand All @@ -43,11 +44,15 @@
"dependencies": {
"@typegoose/typegoose": "11.8.1",
"chalk": "4.1.2",
"class-transformer": "0.5.1",
"convict": "6.2.4",
"convict-format-with-validator": "6.2.0",
"dayjs": "1.11.19",
"dotenv": "17.3.1",
"express": "5.2.1",
"express-async-handler": "1.2.0",
"got": "14.6.6",
"http-status-codes": "2.3.0",
"inversify": "7.11.0",
"mongoose": "7.8.9",
"pino": "8.21.0",
Expand Down
46 changes: 44 additions & 2 deletions src/rest/rest.application.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import express, { Express } from 'express';
import { inject, injectable } from 'inversify';
import { Component } from '../shared/types/index.js';
import { Logger } from '../shared/libs/logger/index.js';
import { getMongoURI } from '../shared/helpers/index.js';
import { Config, RestSchema } from '../shared/libs/config/index.js';
import { DatabaseClient } from '../shared/libs/database-client/index.js';
import { Controller, ExceptionFilter } from '../shared/libs/rest/index.js';

@injectable()
export class RestApplication {
private readonly server: Express;

constructor(
@inject(Component.Logger) private readonly logger: Logger,
@inject(Component.Config) private readonly config: Config<RestSchema>,
@inject(Component.DatabaseClient) private readonly databaseClient: DatabaseClient,
) {}
@inject(Component.ExceptionFilter) private readonly exceptionFilter: ExceptionFilter,
@inject(Component.UserController) private readonly userController: Controller,
@inject(Component.OfferController) private readonly offerController: Controller,
) {
this.server = express();
}

private async initDb() {
const mongoUri = getMongoURI(
Expand All @@ -25,12 +34,45 @@ export class RestApplication {
return this.databaseClient.connect(mongoUri);
}

private async _initServer() {
const port = this.config.get('PORT');
this.server.listen(port);
}

private async _initControllers() {
this.server.use('/users', this.userController.router);
this.server.use('/offers', this.offerController.router);
}

private async _initExceptionFilters() {
this.server.use(this.exceptionFilter.catch.bind(this.exceptionFilter));
}

private async _initMiddleware() {
this.server.use(express.json());
}

public async init() {
this.logger.info('Application initialization');
this.logger.info(`Get value from env $PORT: ${this.config.get('PORT')}`);

this.logger.info('Init database…');
await this.initDb();
this.logger.info('Init database completed');

this.logger.info('Init app-level middleware');
await this._initMiddleware();
this.logger.info('App-level middleware initialization completed');

this.logger.info('Init controllers');
await this._initControllers();
this.logger.info('Controller initialization completed');

this.logger.info('Init exception filters');
await this._initExceptionFilters();
this.logger.info('Exception filters initialization completed');

this.logger.info('Try to init server…');
await this._initServer();
this.logger.info(`🚀 Server started on http://localhost:${this.config.get('PORT')}`);
}
}
2 changes: 2 additions & 0 deletions src/rest/rest.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Component } from '../shared/types/index.js';
import { RestApplication } from './rest.application.js';
import { Logger, PinoLogger } from '../shared/libs/logger/index.js';
import { Config, RestConfig, RestSchema } from '../shared/libs/config/index.js';
import { AppExceptionFilter, ExceptionFilter } from '../shared/libs/rest/index.js';
import { DatabaseClient, MongoDatabaseClient } from '../shared/libs/database-client/index.js';

export function createRestApplicationContainer() {
Expand All @@ -12,6 +13,7 @@ export function createRestApplicationContainer() {
restApplicationContainer.bind<Logger>(Component.Logger).to(PinoLogger).inSingletonScope();
restApplicationContainer.bind<Config<RestSchema>>(Component.Config).to(RestConfig).inSingletonScope();
restApplicationContainer.bind<DatabaseClient>(Component.DatabaseClient).to(MongoDatabaseClient).inSingletonScope();
restApplicationContainer.bind<ExceptionFilter>(Component.ExceptionFilter).to(AppExceptionFilter).inSingletonScope();

return restApplicationContainer;
}
12 changes: 12 additions & 0 deletions src/shared/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ClassConstructor, plainToInstance } from 'class-transformer';

export function generateRandomValue(min:number, max: number, numAfterDigit = 0) {
return +((Math.random() * (max - min)) + min).toFixed(numAfterDigit);
}
Expand All @@ -15,3 +17,13 @@ export function getRandomItem<T>(items: T[]):T {
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : '';
}

export function fillDTO<T, V>(someDto: ClassConstructor<T>, plainObject: V) {
return plainToInstance(someDto, plainObject, { excludeExtraneousValues: true });
}

export function createErrorObject(message: string) {
return {
error: message,
};
}
49 changes: 49 additions & 0 deletions src/shared/libs/rest/controller/base-controller.abstract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { injectable } from 'inversify';
import { Response, Router } from 'express';
import { Logger } from '../../logger/index.js';
import { StatusCodes } from 'http-status-codes';
import asyncHandler from 'express-async-handler';
import { Route } from '../types/route.interface.js';
import { Controller } from './controller.interface.js';

const DEFAULT_CONTENT_TYPE = 'application/json';

@injectable()
export abstract class BaseController implements Controller {
private readonly _router: Router;

constructor(
protected readonly logger: Logger
) {
this._router = Router();
}

get router() {
return this._router;
}

public addRoute(route: Route) {
const wrapperAsyncHandler = asyncHandler(route.handler.bind(this));
this._router[route.method](route.path, wrapperAsyncHandler);
this.logger.info(`Route registered: ${route.method.toUpperCase()} ${route.path}`);
}

public send<T>(res: Response, statusCode: number, data: T): void {
res
.type(DEFAULT_CONTENT_TYPE)
.status(statusCode)
.json(data);
}

public created<T>(res: Response, data: T): void {
this.send(res, StatusCodes.CREATED, data);
}

public noContent<T>(res: Response, data: T): void {
this.send(res, StatusCodes.NO_CONTENT, data);
}

public ok<T>(res: Response, data: T): void {
this.send(res, StatusCodes.OK, data);
}
}
11 changes: 11 additions & 0 deletions src/shared/libs/rest/controller/controller.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Response, Router } from 'express';
import { Route } from '../types/route.interface.js';

export interface Controller {
readonly router: Router;
addRoute(route: Route): void;
send<T>(res: Response, statusCode: number, data: T): void;
ok<T>(res: Response, data: T): void;
created<T>(res: Response, data: T): void;
noContent<T>(res: Response, data: T): void;
}
12 changes: 12 additions & 0 deletions src/shared/libs/rest/errors/http-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class HttpError extends Error {
public httpStatusCode!: number;
public detail?: string;

constructor(httpStatusCode: number, message: string, detail?: string) {
super(message);

this.httpStatusCode = httpStatusCode;
this.message = message;
this.detail = detail;
}
}
1 change: 1 addition & 0 deletions src/shared/libs/rest/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './http-error.js';
39 changes: 39 additions & 0 deletions src/shared/libs/rest/exception-filter/app-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Logger } from '../../logger/index.js';
import { inject, injectable } from 'inversify';
import { HttpError } from '../errors/index.js';
import { StatusCodes } from 'http-status-codes';
import { Component } from '../../../types/index.js';
import { NextFunction, Request, Response } from 'express';
import { createErrorObject } from '../../../helpers/index.js';
import { ExceptionFilter } from './exception-filter.interface.js';

@injectable()
export class AppExceptionFilter implements ExceptionFilter {
constructor(
@inject(Component.Logger) private readonly logger: Logger
) {
this.logger.info('Register AppExceptionFilter');
}

private handleHttpError(error: HttpError, _req: Request, res: Response, _next: NextFunction) {
this.logger.error(`[${error.detail}]: ${error.httpStatusCode} — ${error.message}`, error);
res
.status(error.httpStatusCode)
.json(createErrorObject(error.message));
}

private handleOtherError(error: Error, _req: Request, res: Response, _next: NextFunction) {
this.logger.error(error.message, error);
res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json(createErrorObject(error.message));
}

public catch(error: Error | HttpError, req: Request, res: Response, next: NextFunction): void {
if (error instanceof HttpError) {
return this.handleHttpError(error, req, res, next);
}

this.handleOtherError(error, req, res, next);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NextFunction, Request, Response } from 'express';

export interface ExceptionFilter {
catch(error: Error, req: Request, res: Response, next:NextFunction): void;
}
9 changes: 9 additions & 0 deletions src/shared/libs/rest/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './errors/index.js';
export * from './types/route.interface.js';
export * from './types/http-method.enum.js';
export * from './types/request-body.type.js';
export * from './types/request.params.type.js';
export * from './controller/controller.interface.js';
export * from './controller/base-controller.abstract.js';
export * from './exception-filter/app-exception.filter.js';
export * from './exception-filter/exception-filter.interface.js';
7 changes: 7 additions & 0 deletions src/shared/libs/rest/types/http-method.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum HttpMethod {
Get = 'get',
Post = 'post',
Delete = 'delete',
Patch = 'patch',
Put = 'put',
}
1 change: 1 addition & 0 deletions src/shared/libs/rest/types/request-body.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type RequestBody = Record<string, unknown>;
1 change: 1 addition & 0 deletions src/shared/libs/rest/types/request.params.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type RequestParams = Record<string, unknown>;
8 changes: 8 additions & 0 deletions src/shared/libs/rest/types/route.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { HttpMethod } from './http-method.enum.js';
import { NextFunction, Request, Response } from 'express';

export interface Route {
path: string;
method: HttpMethod;
handler: (req: Request, res: Response, next: NextFunction) => void;
}
5 changes: 5 additions & 0 deletions src/shared/modules/offer/create-offer-request.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Request } from 'express';
import { CreateOfferDto } from './dto/create-offer.dto.js';
import { RequestBody, RequestParams } from '../../libs/rest/index.js';

export type CreateOfferRequest = Request<RequestParams, RequestBody, CreateOfferDto>;
1 change: 1 addition & 0 deletions src/shared/modules/offer/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './offer.entity.js';
export * from './offer.container.js';
export * from './offer.controller.js';
export * from './dto/create-offer.dto.js';
export * from './default-offer.service.js';
export * from './offer-service.interface.js';
4 changes: 4 additions & 0 deletions src/shared/modules/offer/offer-by-id-request.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Request } from 'express';
import { RequestBody, RequestParams } from '../../libs/rest/index.js';

export type OfferByIdRequest = Request<RequestParams, RequestBody, unknown>;
5 changes: 4 additions & 1 deletion src/shared/modules/offer/offer.container.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Container } from 'inversify';
import { types } from '@typegoose/typegoose';
import { Component } from '../../types/index.js';
import { Controller } from '../../libs/rest/index.js';
import { OfferController } from './offer.controller.js';
import { OfferService } from './offer-service.interface.js';
import { OfferEntity, OfferModel } from './offer.entity.js';
import { DefaultOfferService } from './default-offer.service.js';

export function createOfferContainer(parent?: Container) {
const offerContainer = new Container(parent ? { parent } : undefined);

offerContainer.bind<OfferService>(Component.OfferService).to(DefaultOfferService);
offerContainer.bind<OfferService>(Component.OfferService).to(DefaultOfferService).inSingletonScope();
offerContainer.bind<types.ModelType<OfferEntity>>(Component.OfferModel).toConstantValue(OfferModel);
offerContainer.bind<Controller>(Component.OfferController).to(OfferController).inSingletonScope();

return offerContainer;
}
Loading
Loading