Создание REST API с NestJS и TypeORM

в 18:29, , рубрики: NEST, nest.js, nestjs, Node, node.js, nodejs, postgresql, swagger, TypeScript

Содержание

  1. Введение

  2. Установка и настройка проекта

  3. Создание модуля и сущности

  4. Создание DTO и валидация

  5. Создание сервиса и контроллера

  6. Реализация CRUD операций

  7. Тестирование API

  8. Заключение

Введение

NestJS — это прогрессивный фреймворк для построения эффективных и масштабируемых серверных приложений на Node.js. Он использует современные возможности JavaScript и TypeScript, вдохновлен архитектурными паттернами Angular и поддерживает модульность, инъекцию зависимостей и другие современные подходы.

TypeORM — это ORM (Object-Relational Mapping) инструмент, который позволяет взаимодействовать с базами данных, используя объекты и классы, что упрощает разработку и поддерживает различные СУБД, такие как PostgreSQL, MySQL, SQLite и другие.

Сочетание NestJS и TypeORM предоставляет мощный инструментарий для разработки REST API, обеспечивая высокую производительность, модульность и удобство поддержки кода.

Установка и настройка проекта

Установка NestJS CLI

Для начала установим NestJS CLI глобально на вашу машину:

npm install -g @nestjs/cli

Создание нового проекта

Создадим новый проект NestJS с именем my-nestjs-api:

nest new my-nestjs-api

При выполнении команды CLI предложит выбрать пакетный менеджер (npm или yarn). Выберите предпочтительный вариант.

Установка TypeORM и необходимых зависимостей

Перейдите в директорию проекта и установите TypeORM вместе с выбранным драйвером базы данных. В этом примере будем использовать PostgreSQL:

cd my-nestjs-api
npm install --save @nestjs/typeorm typeorm pg

Настройка TypeORM

Откройте файл src/app.module.ts и настройте подключение к базе данных:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'your_username', 
      password: 'your_password', 
      database: 'your_database', 
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true, 
    }),
    UsersModule,
  ],
})
export class AppModule {}

Примечание: Параметр synchronize: true автоматически синхронизирует структуру базы данных с сущностями. Для продакшен-окружения рекомендуется отключить этот параметр и использовать миграции.

Создание модуля и сущности

Создание модуля Users

Используем CLI NestJS для создания модуля, сервиса и контроллера для пользователей:

nest generate module users
nest generate service users
nest generate controller users

Определение сущности User

Создадим файл user.entity.ts в директории src/users/:

// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 100 })
  name: string;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;
}

Объяснение:

  • @Entity() — декоратор, который указывает, что класс является сущностью базы данных.

  • @PrimaryGeneratedColumn() — автоматически генерируемый первичный ключ.

  • @Column() — колонка в таблице базы данных. Можно указывать дополнительные опции, такие как length и unique.

Подключение сущности к модулю

Обновите файл users.module.ts для подключения сущности:

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

Создание DTO и валидация

DTO (Data Transfer Object) используются для определения структуры данных, которые передаются через API. Это помогает обеспечить типизацию и валидацию входящих данных.

Установка библиотек для валидации

NestJS использует библиотеку class-validator для валидации данных. Установим её вместе с class-transformer:

npm install --save class-validator class-transformer

Создание DTO для создания пользователя

Создадим файл create-user.dto.ts в директории src/users/:

// src/users/create-user.dto.ts
import { IsString, IsEmail, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  readonly name: string;

  @IsEmail()
  readonly email: string;

  @IsString()
  @MinLength(6)
  readonly password: string;
}

Создание DTO для обновления пользователя

Создадим файл update-user.dto.ts:

// src/users/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

Объяснение:

  • PartialType позволяет создать DTO, где все свойства опциональны, что удобно для операций обновления.

Применение DTO в контроллере

Обновим контроллер users.controller.ts для использования DTO и валидации:

// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param, Delete, Put, ParseIntPipe } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './create-user.dto';
import { UpdateUserDto } from './update-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }

  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id);
  }
}

Примечание: Использование ParseIntPipe гарантирует, что параметр id будет преобразован в число и валиден.

Создание сервиса и контроллера

Реализация UsersService

Обновим файл users.service.ts для реализации бизнес-логики:

// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto } from './create-user.dto';
import { UpdateUserDto } from './update-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto): Promise<User> {
    const user = this.usersRepository.create(createUserDto);
    return this.usersRepository.save(user);
  }

  async findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  async findOne(id: number): Promise<User> {
    const user = await this.usersRepository.findOneBy({ id });
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
    await this.usersRepository.update(id, updateUserDto);
    return this.findOne(id);
  }

  async remove(id: number): Promise<void> {
    const result = await this.usersRepository.delete(id);
    if (result.affected === 0) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
  }
}

Объяснение:

  • Метод create: Создает нового пользователя и сохраняет его в базе данных.

  • Метод findAll: Возвращает список всех пользователей.

  • Метод findOne: Находит пользователя по ID. Если пользователь не найден, выбрасывает исключение NotFoundException.

  • Метод update: Обновляет данные пользователя и возвращает обновленный объект.

  • Метод remove: Удаляет пользователя по ID. Если пользователь не найден, выбрасывает исключение NotFoundException.

Обновление контроллера для использования сервиса

Контроллер уже был обновлен ранее для использования сервиса. Однако убедимся, что всё настроено корректно:

// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param, Delete, Put, ParseIntPipe } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './create-user.dto';
import { UpdateUserDto } from './update-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }

  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id);
  }
}

Реализация CRUD операций

Теперь, когда сервис и контроллер настроены, мы можем выполнять операции CRUD (Create, Read, Update, Delete) через наше API.

Создание пользователя (Create)

HTTP Метод: POST
URL: /users
Тело запроса:

{
  "name": "Иван Иванов",
  "email": "ivan@example.com",
  "password": "securepassword"
}

Пример с использованием cURL:

curl -X POST http://localhost:3000/users 
-H "Content-Type: application/json" 
-d '{"name": "Иван Иванов", "email": "ivan@example.com", "password": "securepassword"}'

Ответ:

{
  "id": 1,
  "name": "Иван Иванов",
  "email": "ivan@example.com",
  "password": "securepassword"
}

Получение списка пользователей (Read All)

HTTP Метод: GET
URL: /users

Пример с использованием cURL:

curl http://localhost:3000/users

Ответ:

[
  {
    "id": 1,
    "name": "Иван Иванов",
    "email": "ivan@example.com",
    "password": "securepassword"
  }
]

Получение пользователя по ID (Read One)

HTTP Метод: GET
URL: /users/1

Пример с использованием cURL:

curl http://localhost:3000/users/1

Ответ:

{
  "id": 1,
  "name": "Иван Иванов",
  "email": "ivan@example.com",
  "password": "securepassword"
}

Обновление пользователя (Update)

HTTP Метод: PUT
URL: /users/1
Тело запроса:

{
  "name": "Иван Сергеевич Иванов"
}

Пример с использованием cURL:

curl -X PUT http://localhost:3000/users/1 
-H "Content-Type: application/json" 
-d '{"name": "Иван Сергеевич Иванов"}'

Ответ:

{
  "id": 1,
  "name": "Иван Сергеевич Иванов",
  "email": "ivan@example.com",
  "password": "securepassword"
}

Удаление пользователя (Delete)

HTTP Метод: DELETE
URL: /users/1

Пример с использованием cURL:

curl -X DELETE http://localhost:3000/users/1

Ответ: (Статус 200 OK без тела)

Тестирование API

Использование Postman

Postman — популярный инструмент для тестирования API. Вы можете использовать его для отправки запросов к вашему API и проверки ответов.

  1. Создайте новую коллекцию в Postman для вашего проекта.

  2. Добавьте запросы для каждого из CRUD операций:

    • POST /users для создания пользователя.

    • GET /users для получения списка пользователей.

    • GET /users/:id для получения пользователя по ID.

    • PUT /users/:id для обновления пользователя.

    • DELETE /users/:id для удаления пользователя.

  3. Отправляйте запросы и проверяйте ответы, убеждаясь, что API работает корректно.

Написание e2e тестов

NestJS поддерживает написание e2e (end-to-end) тестов с использованием библиотеки supertest. Давайте создадим простой тест для создания пользователя.

Установка дополнительных зависимостей:

npm install --save-dev supertest

Создание e2e теста:

Создайте файл users.e2e-spec.ts в директории test/:

// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('UsersController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    // Включим валидацию DTO
    app.useGlobalPipes(new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }));
    await app.init();
  });

  it('/users (POST)', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ name: 'Тестовый Пользователь', email: 'test@example.com', password: 'test123' })
      .expect(201)
      .then((response) => {
        expect(response.body).toHaveProperty('id');
        expect(response.body.name).toBe('Тестовый Пользователь');
        expect(response.body.email).toBe('test@example.com');
      });
  });

  it('/users (GET)', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200)
      .then((response) => {
        expect(Array.isArray(response.body)).toBeTruthy();
        expect(response.body.length).toBeGreaterThan(0);
      });
  });

  it('/users/:id (GET)', () => {
    return request(app.getHttpServer())
      .get('/users/1')
      .expect(200)
      .then((response) => {
        expect(response.body).toHaveProperty('id', 1);
      });
  });

  it('/users/:id (PUT)', () => {
    return request(app.getHttpServer())
      .put('/users/1')
      .send({ name: 'Обновленное Имя' })
      .expect(200)
      .then((response) => {
        expect(response.body).toHaveProperty('name', 'Обновленное Имя');
      });
  });

  it('/users/:id (DELETE)', () => {
    return request(app.getHttpServer())
      .delete('/users/1')
      .expect(200);
  });

  afterAll(async () => {
    await app.close();
  });
});

Запуск тестов:

В файле package.json убедитесь, что скрипт для e2e тестов настроен:

"scripts": {
  // ... остальные ключи со значениями
  "test:e2e": "jest --config ./test/jest-e2e.json"
}

Запустите тесты командой:

npm run test:e2e

Примечание: Убедитесь, что база данных для тестов настроена отдельно, чтобы не затронуть данные разработки или продакшена.

Заключение

В этой статье мы рассмотрели, как создать REST API с использованием NestJS и TypeORM. Мы прошли через установку и настройку проекта, создание модулей, сущностей, DTO, сервисов и контроллеров, а также реализовали основные CRUD операции и протестировали наше API.

Дальнейшие шаги и рекомендации

  1. Аутентификация и авторизация:

    • Реализуйте систему аутентификации пользователей с использованием JWT.

    • Ограничьте доступ к определённым маршрутам для авторизованных пользователей.

  2. Валидация и обработка ошибок:

    • Улучшите валидацию входящих данных.

    • Настройте глобальную обработку ошибок для более информативных ответов.

  3. Миграции базы данных:

    • Используйте миграции TypeORM для управления изменениями схемы базы данных в продакшене.

  4. Документация API:

    • Интегрируйте Swagger для автоматической генерации документации вашего API.

    npm install --save @nestjs/swagger swagger-ui-express

    Добавьте в main.ts:

    import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
    
    const config = new DocumentBuilder()
      .setTitle('Users API')
      .setDescription('API для управления пользователями')
      .setVersion('1.0')
      .build();
    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('api', app, document);
  5. Тестирование производительности:

    • Проведите нагрузочное тестирование вашего API, используя инструменты вроде Artillery или JMeter.

  6. Развертывание:

    • Разверните ваше приложение на облачных платформах, таких как AWS, Google Cloud, Heroku или DigitalOcean.

    • Настройте CI/CD для автоматического развертывания при изменениях в коде.

  7. Безопасность:

    • Используйте HTTPS для защиты данных в транзите.

    • Реализуйте защиту от распространённых уязвимостей, таких как XSS, CSRF и SQL-инъекции.

Создание REST API с использованием NestJS и TypeORM предоставляет разработчикам мощный и гибкий инструментарий для быстрого создания масштабируемых и поддерживаемых серверных приложений. Следуя приведённым шагам и рекомендациям, вы сможете создать надёжное и эффективное API, соответствующее современным стандартам разработки.

Если у вас возникли вопросы или предложения, оставляйте их в комментариях ниже!

Полезные ссылки:

Автор: neon4on

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js