Пошук уроків, статей та іншого контенту
Створимо окремий модуль бази даних і правильно організуємо його імпорт та доступ у застосунку.
У NestJS функціональність застосунку організована за допомогою модулів. Модуль бази даних може:
налаштовувати підключення до бази даних;
зберігати сутності та репозиторії;
надавати сервіс для роботи з даними;
приховувати деталі роботи з базою від інших частин застосунку.
Інші модулі не повинні самостійно створювати підключення до бази даних. Замість цього вони імпортують DatabaseModule і використовують експортований сервіс.
Базова схема виглядає так:
AppModule
└── UsersModule
└── DatabaseModuleUsersModule
DatabaseModuleDatabaseServiceМодуль описується класом із декоратором @Module():
@Module({
imports: [],
providers: [],
controllers: [],
exports: [],
})
export class SomeModule {}Основні властивості:
imports — модулі, чиї експортовані залежності потрібні поточному модулю;
providers — сервіси та інші залежності, які створює модуль;
controllers — контролери цього модуля;
exports — залежності, доступні модулям, які імпортують поточний модуль.
Важливо: провайдер, оголошений у модулі, не стає автоматично доступним у всьому застосунку. Щоб інший модуль міг його використати, провайдер потрібно додати до exports.
Для прикладу використаємо SQLite та TypeORM. SQLite зберігатиме дані у файлі, тому для запуску не потрібен окремий сервер бази даних.
Встановіть залежності:
npm install @nestjs/typeorm typeorm sqlite3Створимо структуру файлів:
src/
├── database/
│ ├── database.module.ts
│ ├── database.service.ts
│ └── user.entity.ts
├── users/
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
├── app.module.ts
└── main.tsФайл src/database/user.entity.ts:
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}Декоратор @Entity() повідомляє TypeORM, що клас відповідає таблиці в базі даних.
Декоратори властивостей описують колонки:
@PrimaryGeneratedColumn() — первинний ключ із автоматичним збільшенням;
@Column() — звичайна колонка;
unique: true — значення колонки має бути унікальним.
Файл src/database/database.service.ts:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class DatabaseService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
findUsers(): Promise<User[]> {
return this.userRepository.find();
}
createUser(name: string, email: string): Promise<User> {
const user = this.userRepository.create({ name, email });
return this.userRepository.save(user);
}
}@InjectRepository(User) передає в сервіс репозиторій для роботи із сутністю User.
Репозиторій TypeORM містить готові методи:
find() — отримати записи;
create() — створити об'єкт сутності в пам'яті;
save() — зберегти сутність у базі даних.
Файл src/database/database.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseService } from './database.service';
import { User } from './user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'database.sqlite',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}Тут відбувається кілька важливих дій.
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'database.sqlite',
entities: [User],
synchronize: true,
})TypeOrmModule.forRoot() створює основне підключення до бази даних.
У прикладі:
type: 'sqlite' — використовується SQLite;
database: 'database.sqlite' — шлях до файлу бази даних;
entities: [User] — сутність, яку використовує TypeORM;
synchronize: true — TypeORM автоматично синхронізує структуру таблиць із сутностями.
Synchronize: true зручно використовувати під час навчання та локальної розробки. У production-застосунках цю опцію зазвичай не вмикають, щоб випадкова зміна сутності не змінила структуру важливої бази даних.
TypeOrmModule.forFeature([User])Цей виклик реєструє репозиторій User у поточному модулі. Саме тому DatabaseService може отримати його через @InjectRepository(User).
exports: [DatabaseService]Це робить DatabaseService доступним для модулів, які імпортують DatabaseModule.
Зовнішні модулі не повинні знати, що всередині використовується TypeORM або конкретний репозиторій. Вони працюють із методами DatabaseService.
Створимо сервіс користувачів.
Файл src/users/users.service.ts:
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
@Injectable()
export class UsersService {
constructor(private readonly databaseService: DatabaseService) {}
getAll() {
return this.databaseService.findUsers();
}
create(name: string, email: string) {
return this.databaseService.createUser(name, email);
}
}UsersService залежить від DatabaseService, але не працює з Repository<User> безпосередньо.
Модуль користувачів імпортує DatabaseModule.
Файл src/users/users.module.ts:
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [DatabaseModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}Після цього NestJS може створити UsersService, тому що:
UsersModule імпортує DatabaseModule;
DatabaseModule експортує DatabaseService;
DatabaseService доступний у UsersModule.
Файл src/users/users.controller.ts:
import { Body, Controller, Get, Post } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
getUsers() {
return this.usersService.getAll();
}
@Post()
createUser(
@Body() body: { name: string; email: string },
) {
return this.usersService.create(body.name, body.email);
}
}Контролер передає роботу з даними до UsersService, а той — до DatabaseService.
Такий ланцюжок розділяє відповідальність:
UsersController
↓
UsersService
↓
DatabaseService
↓
TypeORM Repository
↓
SQLiteФайл src/app.module.ts:
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [DatabaseModule, UsersModule],
})
export class AppModule {}У цьому прикладі DatabaseModule уже імпортується всередині UsersModule, тому для роботи UsersModule достатньо такого варіанта:
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule {}Однак явний імпорт DatabaseModule в AppModule також допустимий. NestJS не створить друге незалежне підключення, якщо модуль використовується як той самий модуль у графі залежностей.
Для невеликих застосунків часто зручно імпортувати DatabaseModule у функціональні модулі, які ним користуються. Так залежність видно безпосередньо в UsersModule.
Нижче наведено мінімальний набір файлів для запуску.
src/main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();src/app.module.ts:
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule {}src/database/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/database/database.service.ts:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class DatabaseService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
findUsers(): Promise<User[]> {
return this.userRepository.find();
}
createUser(name: string, email: string): Promise<User> {
const user = this.userRepository.create({ name, email });
return this.userRepository.save(user);
}
}src/database/database.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseService } from './database.service';
import { User } from './user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'database.sqlite',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
],
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}src/users/users.service.ts:
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
@Injectable()
export class UsersService {
constructor(private readonly databaseService: DatabaseService) {}
getAll() {
return this.databaseService.findUsers();
}
create(name: string, email: string) {
return this.databaseService.createUser(name, email);
}
}src/users/users.controller.ts:
import { Body, Controller, Get, Post } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
getUsers() {
return this.usersService.getAll();
}
@Post()
createUser(
@Body() body: { name: string; email: string },
) {
return this.usersService.create(body.name, body.email);
}
}src/users/users.module.ts:
import { Module } from '@nestjs/common';
import { DatabaseModule } from '../database/database.module';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
imports: [DatabaseModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}Запуск:
npm run start:devСтворення користувача:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Олена","email":"olena@example.com"}'Отримання користувачів:
curl http://localhost:3000/usersПісля першого запуску в корені проєкту з'явиться файл database.sqlite.
Розглянемо спрощену схему:
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}Якщо інший модуль хоче використати DatabaseService, він має імпортувати DatabaseModule:
@Module({
imports: [DatabaseModule],
providers: [UsersService],
})
export class UsersModule {}Не потрібно повторно додавати DatabaseService до providers у UsersModule:
@Module({
imports: [DatabaseModule],
providers: [DatabaseService, UsersService],
})
export class UsersModule {}Такий варіант неправильний для цієї структури. Він створює окремий екземпляр сервісу в UsersModule і обходить механізм експорту та імпорту.
Правильне правило:
провайдер оголошується у власному модулі;
модуль експортує його;
інший модуль імпортує модуль, а не копіює провайдер.
exportsЯкщо DatabaseModule має:
@Module({
providers: [DatabaseService],
})
export class DatabaseModule {}то UsersModule не зможе отримати DatabaseService.
Потрібно додати:
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}Якщо UsersModule використовує DatabaseService, але не імпортує DatabaseModule, NestJS не зможе розв'язати залежність.
@Module({
imports: [DatabaseModule],
})
export class UsersModule {}forFeatureЯкщо сервіс містить:
@InjectRepository(User)
private readonly userRepository: Repository<User>у модулі має бути:
TypeOrmModule.forFeature([User])Без цього NestJS не знатиме, який репозиторій потрібно впровадити.
DatabaseService оголошено в неправильному модуліНе варто оголошувати сервіс бази даних у кожному функціональному модулі окремо. Це ускладнює структуру застосунку та може призвести до створення кількох незалежних екземплярів.
Краще зберігати його в DatabaseModule і експортувати звідти.
synchronize: true використовується у productionЦя опція зручна під час навчання, але автоматична синхронізація структури бази даних може бути небезпечною для реального застосунку. Для production слід використовувати контрольовані зміни схеми бази даних.
DatabaseModule ізолює налаштування бази даних від інших модулів.
imports підключає залежності до модуля.
providers реєструє сервіси модуля.
exports відкриває вибрані сервіси для інших модулів.
Щоб скористатися DatabaseService, функціональний модуль має імпортувати DatabaseModule.
Репозиторії TypeORM реєструються через TypeOrmModule.forFeature().
Інші модулі повинні працювати з публічним сервісом бази даних, а не створювати підключення самостійно.