Пошук уроків, статей та іншого контенту
Розглянемо шари доступу до даних, відповідальність модулів і потік запиту від контролера до бази.
У NestJS запит до бази даних зазвичай проходить через кілька рівнів:
Контролер приймає HTTP-запит і повертає HTTP-відповідь.
Сервіс реалізує бізнес-логіку застосунку.
Репозиторій або ORM виконує операції читання та запису.
База даних зберігає інформацію.
Наприклад, для створення користувача потік виглядає так:
HTTP POST /users
↓
UsersController
↓
UsersService
↓
Repository
↓
DatabaseКожен шар має власну відповідальність. Завдяки цьому код простіше читати, тестувати й змінювати.
Контролер працює з HTTP:
приймає параметри маршруту;
читає тіло запиту;
викликає потрібний метод сервісу;
повертає результат клієнту.
Контролер не повинен:
самостійно виконувати SQL-запити;
містити складну бізнес-логіку;
знати деталі структури бази даних.
@Post()
create(@Body() data: CreateUserDto) {
return this.usersService.create(data);
}Контролер лише передає дані сервісу.
Сервіс містить логіку прикладної операції:
перевіряє умови;
викликає репозиторій;
комбінує кілька операцій;
формує результат для контролера.
Наприклад, сервіс може перевірити, чи не зареєстрована вже електронна адреса, перед створенням користувача.
Сервіс не повинен залежати від HTTP. Він має працювати незалежно від того, викликав його контролер, фонове завдання чи тест.
Репозиторій відповідає за доступ до даних:
пошук записів;
створення записів;
оновлення;
видалення.
Репозиторій приховує деталі конкретного способу зберігання. Сервісу не потрібно знати, чи використовується PostgreSQL, SQLite або інша база даних.
У NestJS роль репозиторію часто виконує ORM-репозиторій, наприклад Repository<User> з TypeORM.
Сутність описує структуру запису в базі даних. У TypeORM клас із декоратором @Entity() відповідає таблиці.
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}Сутність описує дані, але не повинна містити логіку HTTP-запитів.
Модуль NestJS об'єднує пов'язані частини функціональності:
контролери;
сервіси;
сутності;
провайдери;
налаштування доступу до бази даних.
Для функціональності користувачів зазвичай створюють UsersModule.
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}TypeOrmModule.forFeature([User]) реєструє репозиторій сутності User у цьому модулі.
Після цього NestJS може передати репозиторій у сервіс через dependency injection.
Глобальне підключення до бази даних зазвичай налаштовують в AppModule.
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'data.sqlite',
entities: [User],
synchronize: true,
}),
UsersModule,
],
})
export class AppModule {}Основні параметри:
type — тип бази даних;
database — назва файлу для SQLite або інші параметри підключення;
entities — сутності, які використовує застосунок;
synchronize — автоматичне створення або оновлення таблиць.
सynchronize: true зручно використовувати під час навчання. У production-середовищі його зазвичай не вмикають, оскільки автоматична зміна структури таблиць може бути небезпечною.
Нижче наведено мінімальну структуру застосунку користувачів із NestJS і TypeORM.
Для запуску потрібні пакети:
npm install @nestjs/typeorm typeorm sqlite3Файл src/users/user.entity.ts:
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}Файл src/users/create-user.dto.ts:
export class CreateUserDto {
name: string;
email: string;
}DTO описує дані, які очікує HTTP-метод створення користувача.
Файл src/users/users.service.ts:
import { ConflictException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateUserDto } from './create-user.dto';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}
async create(data: CreateUserDto): Promise<User> {
const existingUser = await this.usersRepository.findOneBy({
email: data.email,
});
if (existingUser) {
throw new ConflictException('Користувач із такою електронною адресою вже існує');
}
const user = this.usersRepository.create(data);
return this.usersRepository.save(user);
}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
}Сервіс:
отримує репозиторій через dependency injection;
перевіряє, чи існує користувач;
створює об'єкт сутності;
зберігає його в базі даних;
повертає результат.
Методи findOneBy, create, save і find належать репозиторію TypeORM.
Файл src/users/users.controller.ts:
import { Body, Controller, Get, Post } from '@nestjs/common';
import { CreateUserDto } from './create-user.dto';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() data: CreateUserDto) {
return this.usersService.create(data);
}
@Get()
findAll() {
return this.usersService.findAll();
}
}Контролер не створює SQL-запити. Він передає дані сервісу та повертає результат.
Файл src/users/users.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}Цей модуль реєструє:
User як сутність TypeORM;
UsersController як контролер;
UsersService як провайдер;
репозиторій User через TypeOrmModule.forFeature.
Файл src/app.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './users/user.entity';
import { UsersModule } from './users/users.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'data.sqlite',
entities: [User],
synchronize: true,
}),
UsersModule,
],
})
export class AppModule {}Після запуску застосунку можна створити користувача:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Олена","email":"olena@example.com"}'У відповідь застосунок поверне створений запис:
{
"name": "Олена",
"email": "olena@example.com",
"id": 1
}Отримати всіх користувачів можна запитом:
curl http://localhost:3000/usersУ сервісі вказано:
constructor(
@InjectRepository(User)
private readonly usersRepository: Repository<User>,
) {}NestJS бачить, що сервісу потрібен репозиторій User, знаходить його серед провайдерів модуля та передає в конструктор.
Це називається dependency injection, або впровадження залежностей.
Завдяки цьому сервіс не створює репозиторій вручну. Він лише описує, яка залежність йому потрібна.
Переваги такого підходу:
менше зв'язаності між класами;
простіше замінити реалізацію;
простіше тестувати сервіс;
залежності видно в конструкторі.
Розглянемо запит:
POST /usersіз тілом:
{
"name": "Олена",
"email": "olena@example.com"
}Потік виконання:
NestJS знаходить UsersController, оскільки він має префікс users.
Метод create обробляє HTTP-метод POST.
@Body() передає тіло запиту в метод.
Контролер викликає usersService.create(data).
Сервіс через репозиторій перевіряє наявність користувача.
Якщо користувача немає, сервіс створює сутність.
Репозиторій зберігає сутність у базі даних.
Результат повертається з репозиторію до сервісу.
Сервіс повертає результат контролеру.
NestJS серіалізує об'єкт у JSON-відповідь.
Контролер при цьому не знає, як саме зберігається користувач, а база даних не знає про HTTP-маршрут.
Такий код змішує кілька відповідальностей:
@Post()
async create(@Body() data: CreateUserDto) {
const user = this.usersRepository.create(data);
return this.usersRepository.save(user);
}Проблеми такого підходу:
контролер залежить від конкретного ORM;
бізнес-логіка опиняється в HTTP-шарі;
складніше повторно використати операцію;
складніше тестувати логіку без запуску HTTP-сервера;
контролери стають великими.
Краще передати операцію сервісу:
@Post()
create(@Body() data: CreateUserDto) {
return this.usersService.create(data);
}Модуль повинен відповідати за певну функціональність. Наприклад:
UsersModule — користувачі;
OrdersModule — замовлення;
ProductsModule — товари.
За замовчуванням провайдери модуля доступні лише всередині цього модуля. Якщо сервіс потрібно використати в іншому модулі, його можна експортувати.
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}Після цього інший модуль може імпортувати UsersModule і використовувати UsersService.
Такий підхід допомагає зберігати чіткі межі між частинами застосунку.
Контролер не повинен містити логіку доступу до даних. Передавайте такі операції сервісу.
Сервіс не повинен перетворюватися на один великий клас. Якщо функціональність стає складною, розділяйте її на зрозумілі методи та окремі компоненти.
forFeatureЯкщо використовується @InjectRepository(User), модуль має імпортувати:
TypeOrmModule.forFeature([User])Без цього NestJS не зможе створити потрібну залежність.
Сутність має бути доступною для TypeORM через forRoot:
TypeOrmModule.forRoot({
entities: [User],
})Якщо сутність не зареєстрована, ORM не зможе працювати з відповідною таблицею.
Підключення до бази даних налаштовують один раз на рівні застосунку. Окремі модулі отримують потрібні репозиторії через dependency injection.
synchronize: true без розуміння наслідківЦей параметр зручний під час навчання, але автоматична синхронізація структури бази може змінити або видалити несумісні структури в реальному середовищі.
Контролер відповідає за HTTP-запити та відповіді.
Сервіс містить бізнес-логіку.
Репозиторій відповідає за доступ до даних.
Сутність описує структуру запису в базі даних.
Модуль об'єднує пов'язані контролери, сервіси та репозиторії.
Підключення до бази даних налаштовують у кореневому модулі.
Репозиторії передаються сервісам через dependency injection.
Типовий потік запиту має вигляд:
Контролер → Сервіс → Репозиторій → База данихТаке розділення відповідальності робить NestJS-застосунок передбачуваним і зручним для розвитку.