Repository: js-tasks-ru/nodejs-20250303_amozalev
Pull request #5: SQL CRUD
Author: login: amozalev
Created: 2025-03-30T21:49:44Z | Merged: 2025-03-31T09:16:26Z



Diff:
diff --git a/05-db/01-crud-tasks/app.module.ts b/05-db/01-crud-tasks/app.module.ts
index 3d6655f..83b12c2 100644
--- a/05-db/01-crud-tasks/app.module.ts
+++ b/05-db/01-crud-tasks/app.module.ts
@@ -1,7 +1,20 @@
 import { Module } from "@nestjs/common";
 import { TasksModule } from "./tasks/tasks.module";
+import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
+import { ConfigModule } from "@nestjs/config";
+import database from "./config/database";
+import { DataSource } from "typeorm";
 
 @Module({
-  imports: [TasksModule],
+  imports: [
+    ConfigModule.forRoot({
+      isGlobal: true,
+      load: [database],
+    }),
+    TasksModule,
+    TypeOrmModule.forRootAsync(database.asProvider() as TypeOrmModuleOptions),
+  ],
 })
-export class AppModule {}
+export class AppModule {
+  constructor(private dataSource: DataSource) {}
+}
diff --git a/05-db/01-crud-tasks/config/database.ts b/05-db/01-crud-tasks/config/database.ts
new file mode 100644
index 0000000..651eeef
--- /dev/null
+++ b/05-db/01-crud-tasks/config/database.ts
@@ -0,0 +1,11 @@
+import { registerAs } from "@nestjs/config";
+import { Task } from "../tasks/entities/task.entity";
+
+
+export default registerAs('database', () => ({
+  type: 'sqlite',
+  database: './db.sqlite',
+  entities: [Task],
+  autoloadEntities: true,
+  synchronize: true
+}))
diff --git a/05-db/01-crud-tasks/db.sqlite b/05-db/01-crud-tasks/db.sqlite
index 9492447..b33a226 100644
Binary files a/05-db/01-crud-tasks/db.sqlite and b/05-db/01-crud-tasks/db.sqlite differ
diff --git a/05-db/01-crud-tasks/main.ts b/05-db/01-crud-tasks/main.ts
index e6f39c2..081ebd3 100644
--- a/05-db/01-crud-tasks/main.ts
+++ b/05-db/01-crud-tasks/main.ts
@@ -4,6 +4,11 @@ import { ValidationPipe } from "@nestjs/common";
 
 async function bootstrap() {
   const app = await NestFactory.create(AppModule);
+  app.useGlobalPipes(
+  new ValidationPipe({
+    transform: true,
+  }),
+);
   await app.listen(process.env.PORT ?? 3000);
 }
 bootstrap();
diff --git a/05-db/01-crud-tasks/tasks/dto/create-task.dto.ts b/05-db/01-crud-tasks/tasks/dto/create-task.dto.ts
index 1d3c804..4bf0f54 100644
--- a/05-db/01-crud-tasks/tasks/dto/create-task.dto.ts
+++ b/05-db/01-crud-tasks/tasks/dto/create-task.dto.ts
@@ -1 +1,9 @@
-export class CreateTaskDto {}
+import { IsNotEmpty } from "class-validator";
+
+export class CreateTaskDto {
+  @IsNotEmpty()
+  title: string;
+
+  @IsNotEmpty()
+  description: string;
+}
diff --git a/05-db/01-crud-tasks/tasks/dto/update-task.dto.ts b/05-db/01-crud-tasks/tasks/dto/update-task.dto.ts
index 01fa8e8..717f644 100644
--- a/05-db/01-crud-tasks/tasks/dto/update-task.dto.ts
+++ b/05-db/01-crud-tasks/tasks/dto/update-task.dto.ts
@@ -1,4 +1,4 @@
 import { PartialType } from "@nestjs/mapped-types";
 import { CreateTaskDto } from "./create-task.dto";
 
-export class UpdateTaskDto extends PartialType(CreateTaskDto) {}
+export class UpdateTaskDto extends PartialType(CreateTaskDto) {}
\ No newline at end of file
diff --git a/05-db/01-crud-tasks/tasks/entities/task.entity.ts b/05-db/01-crud-tasks/tasks/entities/task.entity.ts
index bb17efb..8ec135c 100644
--- a/05-db/01-crud-tasks/tasks/entities/task.entity.ts
+++ b/05-db/01-crud-tasks/tasks/entities/task.entity.ts
@@ -1 +1,16 @@
-export class Task {}
+import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+
+@Entity()
+export class Task {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column()
+  title: string;
+
+  @Column({ default: false })
+  isCompleted: boolean;
+
+  @Column({default: ''})
+  description: string;
+}
diff --git a/05-db/01-crud-tasks/tasks/task.model.ts b/05-db/01-crud-tasks/tasks/task.model.ts
new file mode 100644
index 0000000..30b43ba
--- /dev/null
+++ b/05-db/01-crud-tasks/tasks/task.model.ts
@@ -0,0 +1,6 @@
+import { IsNumberString } from "class-validator";
+
+export class FindOneParams {
+  @IsNumberString()
+  id: number;
+}
diff --git a/05-db/01-crud-tasks/tasks/tasks.controller.ts b/05-db/01-crud-tasks/tasks/tasks.controller.ts
index 431892c..24be566 100644
--- a/05-db/01-crud-tasks/tasks/tasks.controller.ts
+++ b/05-db/01-crud-tasks/tasks/tasks.controller.ts
@@ -1,22 +1,64 @@
-import { Controller, Get, Post, Patch, Delete } from "@nestjs/common";
+import {
+  Controller,
+  Get,
+  Post,
+  Patch,
+  Delete,
+  HttpException,
+  HttpStatus,
+  Body,
+  Param,
+  Query,
+} from "@nestjs/common";
 import { TasksService } from "./tasks.service";
+import { CreateTaskDto } from "./dto/create-task.dto";
+import { UpdateTaskDto } from "./dto/update-task.dto";
+import { FindOneParams } from "./task.model";
+
+const TASK_NOT_FOUND_MSG = "Task with such Id does not exist";
 
 @Controller("tasks")
 export class TasksController {
   constructor(private readonly tasksService: TasksService) {}
 
   @Post()
-  create() {}
+  async create(@Body() task: CreateTaskDto) {
+    return await this.tasksService.create(task);
+  }
 
   @Get()
-  findAll() {}
+  findAll(@Query("page") page?: number, @Query("limit") limit?: number) {
+    const numPage = isNaN(page) ? 1 : page;
+    return this.tasksService.findAll(numPage, limit);
+  }
 
   @Get(":id")
-  findOne() {}
+  async findOne(@Param() { id }: FindOneParams) {
+    const task = await this.tasksService.findOne(id);
+    if (task) {
+      return task;
+    }
+
+    throw new HttpException(TASK_NOT_FOUND_MSG, HttpStatus.NOT_FOUND);
+  }
 
   @Patch(":id")
-  update() {}
+  async update(@Param() { id }: FindOneParams, @Body() task: UpdateTaskDto) {
+    const updatedTask = await this.tasksService.update(id, task);
+    if (updatedTask) {
+      return updatedTask;
+    }
+
+    throw new HttpException(TASK_NOT_FOUND_MSG, HttpStatus.NOT_FOUND);
+  }
 
   @Delete(":id")
-  remove() {}
+  async remove(@Param() { id }: FindOneParams) {
+    const task = await this.tasksService.remove(id);
+    if (task) {
+      return task;
+    }
+
+    throw new HttpException(TASK_NOT_FOUND_MSG, HttpStatus.NOT_FOUND);
+  }
 }
diff --git a/05-db/01-crud-tasks/tasks/tasks.module.ts b/05-db/01-crud-tasks/tasks/tasks.module.ts
index 66af18b..adbb9a8 100644
--- a/05-db/01-crud-tasks/tasks/tasks.module.ts
+++ b/05-db/01-crud-tasks/tasks/tasks.module.ts
@@ -1,8 +1,11 @@
 import { Module } from "@nestjs/common";
 import { TasksService } from "./tasks.service";
 import { TasksController } from "./tasks.controller";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { Task } from "./entities/task.entity";
 
 @Module({
+  imports: [TypeOrmModule.forFeature([Task])],
   controllers: [TasksController],
   providers: [TasksService],
 })
diff --git a/05-db/01-crud-tasks/tasks/tasks.service.ts b/05-db/01-crud-tasks/tasks/tasks.service.ts
index 320d181..225c1db 100644
--- a/05-db/01-crud-tasks/tasks/tasks.service.ts
+++ b/05-db/01-crud-tasks/tasks/tasks.service.ts
@@ -1,16 +1,45 @@
-import { Injectable } from "@nestjs/common";
+import { Injectable, NotFoundException } from "@nestjs/common";
 import { CreateTaskDto } from "./dto/create-task.dto";
 import { UpdateTaskDto } from "./dto/update-task.dto";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Task } from "./entities/task.entity";
+import { Repository } from "typeorm";
 
 @Injectable()
 export class TasksService {
-  create(createTaskDto: CreateTaskDto) {}
+  constructor(
+    @InjectRepository(Task) private taskRepository: Repository<Task>,
+  ) {}
 
-  async findAll() {}
+  create(createTaskDto: CreateTaskDto): Promise<CreateTaskDto & Task> {
+    return this.taskRepository.save(createTaskDto);
+  }
 
-  async findOne(id: number) {}
+  async findAll(page = 1, limit?: number): Promise<Task[]> {
+    return this.taskRepository.find(
+      limit ? { skip: (page - 1) * limit, take: limit } : {},
+    );
+  }
 
-  async update(id: number, updateTaskDto: UpdateTaskDto) {}
+  async findOne(id: number): Promise<Task> {
+    return await this.taskRepository.findOne({ where: { id } });
+  }
 
-  async remove(id: number): Promise<void> {}
+  async update(id: number, updatedTask: UpdateTaskDto): Promise<Task> {
+    const res = await this.taskRepository.update(id, updatedTask);
+    if (res?.affected > 0) {
+      return await this.findOne(id);
+    }
+
+    throw new NotFoundException(`Task is not found`);
+  }
+
+  async remove(id: number): Promise<{ message: string }> {
+    const res = await this.taskRepository.delete(id);
+    return res?.affected > 0
+      ? {
+          message: "Task deleted successfully",
+        }
+      : null;
+  }
 }
diff --git a/package.json b/package.json
index e47b01f..3366ce3 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
   "dependencies": {
     "@jest/globals": "^29.7.0",
     "@nestjs/common": "^10.0.0",
+    "@nestjs/config": "^4.0.2",
     "@nestjs/core": "^10.0.0",
     "@nestjs/mapped-types": "^2.0.6",
     "@nestjs/mongoose": "^10.1.0",
@@ -25,7 +26,7 @@
     "reflect-metadata": "^0.2.0",
     "rxjs": "^7.8.1",
     "sqlite3": "^5.1.7",
-    "typeorm": "^0.3.20"
+    "typeorm": "^0.3.21"
   },
   "devDependencies": {
     "@nestjs/cli": "^10.0.0",


Reviews:
user: login: tadjik1
body: В целом ты хорошо справился с задачей, но есть несколько моментов, которые стоит поправить для улучшения качества кода и логики.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-31T07:47:07Z
user: login: amozalev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T07:58:11Z
user: login: tadjik1
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T08:01:06Z
user: login: amozalev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T08:09:05Z
user: login: tadjik1
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T08:31:31Z
user: login: amozalev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T08:32:30Z
user: login: amozalev
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T08:38:15Z
user: login: tadjik1
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-31T09:15:39Z
user: login: tadjik1
body: Спасибо за правки, работа выполнена хорошо 👍 
state: APPROVED
author_association: NONE
submitted_at: 2025-03-31T09:16:05Z

Review comments:
diff_hunk: @@ -1 +1,16 @@
-export class Task {}
+import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+
+@Entity()
+export class Task {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column()
+  title: string;
+
+  @Column({ default: false })
+  isCompleted: boolean;
+
+  @Column({ default: false })
path: 05-db/01-crud-tasks/tasks/entities/task.entity.ts
user: login: tadjik1
body: Мне кажется, здесь ошибка: поле description объявлено как boolean, хотя должно быть string. Исправь, пожалуйста.
created_at: 2025-03-31T06:07:01Z
author_association: NONE
diff_hunk: @@ -1,16 +1,43 @@
 import { Injectable } from "@nestjs/common";
 import { CreateTaskDto } from "./dto/create-task.dto";
 import { UpdateTaskDto } from "./dto/update-task.dto";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Task } from "./entities/task.entity";
+import { Repository } from "typeorm";
 
 @Injectable()
 export class TasksService {
-  create(createTaskDto: CreateTaskDto) {}
+  constructor(
+    @InjectRepository(Task) private taskRepository: Repository<Task>,
+  ) {}
 
-  async findAll() {}
+  create(createTaskDto: CreateTaskDto): Promise<CreateTaskDto & Task> {
+    return this.taskRepository.save(createTaskDto);
+  }
 
-  async findOne(id: number) {}
+  async findAll(page = 1, limit?: number): Promise<Task[]> {
+    return this.taskRepository.find(
+      limit ? { skip: (page - 1) * limit, take: limit } : {},
+    );
+  }
 
-  async update(id: number, updateTaskDto: UpdateTaskDto) {}
+  async findOne(id: number): Promise<Task> {
+    return await this.taskRepository.findOne({ where: { id } });
+  }
 
-  async remove(id: number): Promise<void> {}
+  async update(id: number, updatedTask: UpdateTaskDto): Promise<Task> {
+    const res = await this.taskRepository.update(id, updatedTask);
+    if (res?.affected > 0) {
path: 05-db/01-crud-tasks/tasks/tasks.service.ts
user: login: tadjik1
body: На мой взгляд, стоит явно обработать случай, когда задача не найдена при обновлении, и вернуть понятную ошибку или null.
created_at: 2025-03-31T06:07:01Z
author_association: NONE
diff_hunk: @@ -1,16 +1,43 @@
 import { Injectable } from "@nestjs/common";
 import { CreateTaskDto } from "./dto/create-task.dto";
 import { UpdateTaskDto } from "./dto/update-task.dto";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Task } from "./entities/task.entity";
+import { Repository } from "typeorm";
 
 @Injectable()
 export class TasksService {
-  create(createTaskDto: CreateTaskDto) {}
+  constructor(
+    @InjectRepository(Task) private taskRepository: Repository<Task>,
+  ) {}
 
-  async findAll() {}
+  create(createTaskDto: CreateTaskDto): Promise<CreateTaskDto & Task> {
+    return this.taskRepository.save(createTaskDto);
+  }
 
-  async findOne(id: number) {}
+  async findAll(page = 1, limit?: number): Promise<Task[]> {
+    return this.taskRepository.find(
+      limit ? { skip: (page - 1) * limit, take: limit } : {},
+    );
+  }
 
-  async update(id: number, updateTaskDto: UpdateTaskDto) {}
+  async findOne(id: number): Promise<Task> {
+    return await this.taskRepository.findOne({ where: { id } });
+  }
 
-  async remove(id: number): Promise<void> {}
+  async update(id: number, updatedTask: UpdateTaskDto): Promise<Task> {
+    const res = await this.taskRepository.update(id, updatedTask);
+    if (res?.affected > 0) {
+      return await this.findOne(id);
+    }
+  }
+
+  async remove(id: number): Promise<{message: string} | undefined> {
path: 05-db/01-crud-tasks/tasks/tasks.service.ts
user: login: tadjik1
body: Лучше сделать возвращаемое значение метода remove более однородным: либо всегда возвращать объект с сообщением, либо null, если задача не найдена.
created_at: 2025-03-31T06:07:01Z
author_association: NONE
diff_hunk: @@ -1 +1,16 @@
-export class Task {}
+import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+
+@Entity()
+export class Task {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column()
+  title: string;
+
+  @Column({ default: false })
+  isCompleted: boolean;
+
+  @Column({ default: false })
path: 05-db/01-crud-tasks/tasks/entities/task.entity.ts
user: login: amozalev
body: Так description же и есть string сейчас :)
created_at: 2025-03-31T07:58:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1 +1,16 @@
-export class Task {}
+import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+
+@Entity()
+export class Task {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column()
+  title: string;
+
+  @Column({ default: false })
+  isCompleted: boolean;
+
+  @Column({ default: false })
path: 05-db/01-crud-tasks/tasks/entities/task.entity.ts
user: login: tadjik1
body: Я имею ввиду что default указан как false, т.е. boolean. 
created_at: 2025-03-31T08:01:05Z
author_association: NONE
diff_hunk: @@ -1 +1,16 @@
-export class Task {}
+import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+
+@Entity()
+export class Task {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column()
+  title: string;
+
+  @Column({ default: false })
+  isCompleted: boolean;
+
+  @Column({ default: false })
path: 05-db/01-crud-tasks/tasks/entities/task.entity.ts
user: login: amozalev
body: Ох, туплю. Спасибо! Невнимательность...
created_at: 2025-03-31T08:09:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1 +1,16 @@
-export class Task {}
+import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+
+@Entity()
+export class Task {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column()
+  title: string;
+
+  @Column({ default: false })
+  isCompleted: boolean;
+
+  @Column({ default: false })
path: 05-db/01-crud-tasks/tasks/entities/task.entity.ts
user: login: tadjik1
body: все хорошо :) 
created_at: 2025-03-31T08:31:31Z
author_association: NONE
diff_hunk: @@ -1,16 +1,43 @@
 import { Injectable } from "@nestjs/common";
 import { CreateTaskDto } from "./dto/create-task.dto";
 import { UpdateTaskDto } from "./dto/update-task.dto";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Task } from "./entities/task.entity";
+import { Repository } from "typeorm";
 
 @Injectable()
 export class TasksService {
-  create(createTaskDto: CreateTaskDto) {}
+  constructor(
+    @InjectRepository(Task) private taskRepository: Repository<Task>,
+  ) {}
 
-  async findAll() {}
+  create(createTaskDto: CreateTaskDto): Promise<CreateTaskDto & Task> {
+    return this.taskRepository.save(createTaskDto);
+  }
 
-  async findOne(id: number) {}
+  async findAll(page = 1, limit?: number): Promise<Task[]> {
+    return this.taskRepository.find(
+      limit ? { skip: (page - 1) * limit, take: limit } : {},
+    );
+  }
 
-  async update(id: number, updateTaskDto: UpdateTaskDto) {}
+  async findOne(id: number): Promise<Task> {
+    return await this.taskRepository.findOne({ where: { id } });
+  }
 
-  async remove(id: number): Promise<void> {}
+  async update(id: number, updatedTask: UpdateTaskDto): Promise<Task> {
+    const res = await this.taskRepository.update(id, updatedTask);
+    if (res?.affected > 0) {
path: 05-db/01-crud-tasks/tasks/tasks.service.ts
user: login: amozalev
body: Fixed
created_at: 2025-03-31T08:32:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,43 @@
 import { Injectable } from "@nestjs/common";
 import { CreateTaskDto } from "./dto/create-task.dto";
 import { UpdateTaskDto } from "./dto/update-task.dto";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Task } from "./entities/task.entity";
+import { Repository } from "typeorm";
 
 @Injectable()
 export class TasksService {
-  create(createTaskDto: CreateTaskDto) {}
+  constructor(
+    @InjectRepository(Task) private taskRepository: Repository<Task>,
+  ) {}
 
-  async findAll() {}
+  create(createTaskDto: CreateTaskDto): Promise<CreateTaskDto & Task> {
+    return this.taskRepository.save(createTaskDto);
+  }
 
-  async findOne(id: number) {}
+  async findAll(page = 1, limit?: number): Promise<Task[]> {
+    return this.taskRepository.find(
+      limit ? { skip: (page - 1) * limit, take: limit } : {},
+    );
+  }
 
-  async update(id: number, updateTaskDto: UpdateTaskDto) {}
+  async findOne(id: number): Promise<Task> {
+    return await this.taskRepository.findOne({ where: { id } });
+  }
 
-  async remove(id: number): Promise<void> {}
+  async update(id: number, updatedTask: UpdateTaskDto): Promise<Task> {
+    const res = await this.taskRepository.update(id, updatedTask);
+    if (res?.affected > 0) {
+      return await this.findOne(id);
+    }
+  }
+
+  async remove(id: number): Promise<{message: string} | undefined> {
path: 05-db/01-crud-tasks/tasks/tasks.service.ts
user: login: amozalev
body: Исправил. Подскажи, пожалуйста, как лучше сделать. В TasksService в методе update я возвращаю NotFoundException, а в методе remove, объект или null, но ведь тут тоже можно вернуть NotFoundException вместо null? И, в целом, по необходимости, всегда из сервиса лучше возвращать Exception?
created_at: 2025-03-31T08:38:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,43 @@
 import { Injectable } from "@nestjs/common";
 import { CreateTaskDto } from "./dto/create-task.dto";
 import { UpdateTaskDto } from "./dto/update-task.dto";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Task } from "./entities/task.entity";
+import { Repository } from "typeorm";
 
 @Injectable()
 export class TasksService {
-  create(createTaskDto: CreateTaskDto) {}
+  constructor(
+    @InjectRepository(Task) private taskRepository: Repository<Task>,
+  ) {}
 
-  async findAll() {}
+  create(createTaskDto: CreateTaskDto): Promise<CreateTaskDto & Task> {
+    return this.taskRepository.save(createTaskDto);
+  }
 
-  async findOne(id: number) {}
+  async findAll(page = 1, limit?: number): Promise<Task[]> {
+    return this.taskRepository.find(
+      limit ? { skip: (page - 1) * limit, take: limit } : {},
+    );
+  }
 
-  async update(id: number, updateTaskDto: UpdateTaskDto) {}
+  async findOne(id: number): Promise<Task> {
+    return await this.taskRepository.findOne({ where: { id } });
+  }
 
-  async remove(id: number): Promise<void> {}
+  async update(id: number, updatedTask: UpdateTaskDto): Promise<Task> {
+    const res = await this.taskRepository.update(id, updatedTask);
+    if (res?.affected > 0) {
+      return await this.findOne(id);
+    }
+  }
+
+  async remove(id: number): Promise<{message: string} | undefined> {
path: 05-db/01-crud-tasks/tasks/tasks.service.ts
user: login: tadjik1
body: отличный вопрос! есть разница между "выбросить ошибку" и "вернуть ошибку". throw - прекратит выполнение текущей функции (и всего стэка функций) и будет обработан в ближайшем catch(). return же остановит только выполнение текущей функции. 
далее по контроллер vs. сервис. технически ошибку можно выбросить из любого из методов - и nest ее корректно обработает. однако тут есть небольшой логический нюанс: мы договаривались что http занимается контроллер, а "бизнесом" - сервис. с этой точки зрения ошибка HttpNotFound или BadRequest конечно подпадает под логику именно контроллера. однако я довольно часто вижу как такие ошибки выбрасываются прям из сервиса. пока кода немного, пока проект небольшой, на мой взгляд, это вполне рабочее решение. придираться к нему я точно не буду :) 
created_at: 2025-03-31T09:15:38Z
author_association: NONE

PR comments:
user: login: jsru-1
created_at: 2025-03-30T21:50:59Z
body: Добавляю преподавателя (@tadjik1) для код-ревью.
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-30T22:01:22Z
body: Решение было обновлено, посмотрим что скажет @tadjik1
author_association: CONTRIBUTOR
user: login: jsru-1
created_at: 2025-03-31T08:33:26Z
body: Добавляю преподавателя (@tadjik1) для код-ревью.
author_association: CONTRIBUTOR