В прошлом посте мы начали с пустой конфигурации, поняли, как NestJs работает с маршрутами, контроллерами и сервисами. Увидели, как легко настроить Fastify для оптимизации нашего приложения.
Теперь мы настроим базу данных и ORM для взаимодействия и хранения наших данных. Для базы данных мы используем PostgreSQL, используя docker для создания контейнера по умолчанию для приложения, для ORM мы будем использовать Prisma, потому что это лучший Orm на данный момент для взаимодействия с базой данных.
Контейнер Docker
Теперь, когда мы создали наше приложение, давайте его контейнеризируем.
Начните с создания следующих файлов в корневом каталоге проекта:
-
Dockerfile
— Этот файл будет отвечать за импорт образов Docker, разделение их на среду разработки и среду производства, копирование всех наших файлов и установку зависимостей. -
docker-compose.yml
— Этот файл будет отвечать за определение наших контейнеров, необходимых образов для приложений, других сервисов, томов хранения, переменных окружения и т.д.
Откройте Dockerfile
и добавьте
# Dockerfile
FROM node:alpine As development
WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn add glob rimraf
RUN yarn --only=development
COPY . .
RUN yarn build
FROM node:alpine as production
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn add glob rimraf
RUN yarn --only=production
COPY . .
COPY --from=development /usr/src/app/dist ./dist
CMD ["node", "dist/main"]
Откройте файл docker-compose.yml
и добавьте следующий код
# docker-compose.yml
version: "3.7"
services:
main:
container_name: main
build:
context: .
target: development
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
ports:
- 3000:3000
command: yarn start:dev
env_file:
- .env
networks:
- api
depends_on:
- postgres
postgres:
image: postgres:13
container_name: postgres
networks:
- api
env_file:
- .env
ports:
- 5432:5432
volumes:
- pgdata:/var/lib/postgresql/data
networks:
api:
volumes:
pgdata:
Создайте файл .env
и добавьте учетные данные PostgreSQL
# .env
# PostgreSQL
POSTGRES_USER=nestAuth
POSTGRES_PASSWORD=nestAuth
POSTGRES_DB=nestAuth
По умолчанию Fastify слушает только интерфейс localhost 127.0.0.1
. Для доступа к нашему приложению с других хостов необходимо добавить 0.0.0.0.0
в main.ts
.
// src/main.ts
await app.listen(3000, "0.0.0.0");
Отлично, у нас есть наш докер, и давайте приступим к тестированию. Запустите в терминале для разработки
docker-compose up
И наше приложение запущено ?.
Prisma ORM
Prisma — это ORM с открытым исходным кодом, он используется как альтернатива написанию простого SQL или использованию других инструментов доступа к базе данных, таких как конструкторы SQL запросов (например, knex.js) или ORM (например, TypeORM и Sequelize).
Начните установку Prisma CLI в качестве разработки
yarn add Prisma -D
В качестве лучшей практики вызывайте CLI локально, используя префикс npx
, чтобы создать начальную установку Prisma с помощью команды init
.
npx prisma init
Эта команда создает новый каталог Prisma со следующим содержимым
По умолчанию подключение к базе данных установлено на postgresql
.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Если тип нашего соединения правильный, мы установим DATABASE_URL
в .env
.
DATABASE_URL="postgresql://nestAuth:nestAuth@postgres:5432/nestAuth"
Не забудьте добавить в .env
в .gitignore
и создать .env.example
перед созданием репозитория на Github
Для генерации Prisma Client требуется файл schema.prisma
. COPY prisma ./prisma/
копирует весь каталог Prisma на случай, если вам также понадобятся миграции.
# Dockerfile
FROM node:alpine As development
WORKDIR /usr/src/app
COPY package*.json ./
# Here Prisma folder to the container
COPY prisma ./prisma/
RUN yarn add glob rimraf
RUN yarn --only=development
COPY . .
RUN yarn build
FROM node:alpine as production
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package*.json ./
# Here Prisma folder to the container
COPY prisma ./prisma/
RUN yarn add glob rimraf
RUN yarn --only=production
COPY . .
COPY --from=development /usr/src/app/dist ./dist
CMD ["node", "dist/main"]
Первая модель
Теперь для проверки соединения мы создадим модель User
, внутри schema.prisma
вставка
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Когда модель создана, вы можете сгенерировать файлы миграции SQL и запустить их в базе данных. Здесь я использую migrate dev
для запуска в режиме разработки и задаю имя init
для миграции.
Перед этим вам нужно настроить ваш docker
docker-compose up
и отредактировать файл .env
.
DATABASE_URL="postgresql://nestAuth:nestAuth@localhsot:5432/nestAuth"
Всегда, когда вы запускаете prisma migrate
, вам нужно изменить хост базы данных на localhost
после отката к имени вашего контейнера базы данных.
npx prisma migrate dev --name init
Откат файла .env
DATABASE_URL="postgresql://nestAuth:nestAuth@nestauth:5432/nestAuth"
Хорошие новости, наша конфигурация работает, теперь наша база данных синхронизирована с нашим приложением ?.
Настройка Prisma
Мы хотим абстрагироваться от Prisma Client API для запросов к базе данных в рамках сервиса. Поэтому давайте создадим новый PrismaService
, который позаботится об инстанцировании и PrismaClient
для подключения к вашей базе данных.
Создайте prisma.service.ts
внутри папки src
.
// src/prisma.service.ts
import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on("beforeExit", async () => {
await app.close();
});
}
}
Первый сервис
Теперь мы можем написать пользовательский сервис для выполнения вызовов базы данных. В NestJs CLI есть команда nest g
для генерации сервисов, контроллеров, стратегий и других структур. На данный момент мы запускаем
nest g service users
Перед началом создания сервиса нам нужно сгенерировать типы Prisma Model, мы можем сгенерировать с помощью этого
npx prisma generate
Внутри папки src
команда создает папку users
с файлом users.service.ts
и файлом test users.service.spec.ts
. Чтобы протестировать наше подключение к базе данных, создадим два сервиса
И внутри createUser
нам нужно зашифровать пароль пользователя, поэтому создадим для этого провайдера, в папке src
создадим папку providers
и создадим файл password.ts
.
// src/providers/password.ts
import { Injectable } from "@nestjs/common";
import * as bcrypt from "bcrypt";
const SALT_OR_ROUNDS = 10;
@Injectable()
export class PasswordProvider {
async hashPassword(password: string): Promise<string> {
return bcrypt.hashSync(password, SALT_OR_ROUNDS);
}
async comparePassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compareSync(password, hash);
}
}
Класс имеет два метода, hashPassword
и comparePassword
для шифрования и сравнения пароля с помощью brcypt. Внутри класса UsersService
необходимо добавить в конструктор провайдер PasswordProvider
для использования в методах.
// src/users/users.service.ts
import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma.service";
import { User, Prisma } from "@prisma/client";
import { PasswordProvider } from "src/providers/password";
@Injectable()
export class UsersService {
constructor(
private prisma: PrismaService,
private passwordProvider: PasswordProvider
) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<User | null> {
const user = await this.prisma.user.findUnique({
where: userWhereUniqueInput,
});
delete user.password;
return user;
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
const userExists = await this.prisma.user.findUnique({
where: { email: data.email },
});
if (userExists) {
throw new HttpException("User already exists", HttpStatus.CONFLICT);
}
const passwordHashed = await this.passwordProvider.hashPassword(
data.password
);
const user = await this.prisma.user.create({
data: {
...data,
password: passwordHashed,
},
});
delete user.password;
return user;
}
}
С созданным сервисом давайте создадим контроллер для использования маршрута, который
nest g controller users
Эта команда создает users.controller.ts
и наш тестовый файл внутри src/users
, поэтому давайте создадим две функции в контроллере
// src/users/users.controller.ts
import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { User } from "@prisma/client";
import { UsersService } from "./users.service";
// Set prefix route for this group. Ex.: for get profile /users/8126321
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// Create user -> POST /users
@Post()
async signupUser(
@Body() userData: { name: string; email: string; password: string }
): Promise<User> {
return this.usersService.createUser(userData);
}
// Get user Profile -> GET /users/:id
@Get("/:id")
async profile(@Param("id") id: number): Promise<User> {
return this.usersService.user({ id: Number(id) });
}
}
Внутри файла users.module.ts
нам нужно добавить провайдеры, экспорты и массив контроллеров.
// src/users/users.module.ts
import { Module } from "@nestjs/common";
import { PrismaService } from "src/prisma.service";
import { PasswordProvider } from "src/providers/password";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
@Module({
providers: [PasswordProvider, UsersService, PrismaService],
exports: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
И передайте UsersModule
в AppModule
для использования.
//src/app.module.ts
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { PrismaService } from "./prisma.service";
import { UsersModule } from "./users/users.module";
import { UsersService } from "./users/users.service";
import { PasswordProvider } from "./providers/password";
@Module({
imports: [UsersModule],
controllers: [AppController],
providers: [PrismaService, UsersService, PasswordProvider],
})
export class AppModule {}
Давайте протестируем
Теперь запустим наш контейнер docker
docker-compose up
Вот и все! Приложение запущено ?
Итак, в Postman давайте попробуем использовать маршруты createUser
и getProfile
.
curl --location --request POST 'http://0.0.0.0:3000/users'
--header 'Content-Type: application/json'
--data-raw '{
"email": "test@e3x.com",
"name": "Gabriel Menezes",
"password": "123123"
}'
curl --location --request GET 'http://0.0.0.0:3000/users/37'
До следующего раза
Это все, что мы рассмотрим в этой статье: мы докеризировали приложение, настроили Prisma и создали два маршрута. В следующей части цикла мы создадим и определим Auth-провайдеры для аутентификации в нашем приложении.
Спасибо за чтение!
Перейдите в репозиторий для ознакомления с кодом
mnzsss / nest-auth-explained
? Аутентификация с помощью JWT и Google в Nest.js объяснено
Ссылки
- Настройка проекта NestJS с помощью Docker для Back-End разработки
- Докеризация приложения NestJS с Prisma и PostgreSQL