چگونه اولین برنامه NestJS خود را ایجاد کنیم

آفلاین
user-avatar
عرفان حشمتی
12 دی 1399, خواندن در 8 دقیقه

در این مقاله قصد داریم شما را در توسعه یک برنامه ساده todo در NestJS راهنمایی کنیم و مروری بر این فریمورک داشته باشیم.

NestJS چیست؟

NestJS فریمورکی برای توسعه سریع برنامه‌های بک-اند است که ساختار آن با الهام از انگولار، آن را برای تحقق پروژه‌هایی با MEAN stack عالی می‌کند.

چه زمانی از NestJS استفاده کنیم؟

Nest برای توسعه برنامه‌هایی که تعداد بسیار زیادی از درخواست‌ها را با مقدار کمی داده پردازش شده در سمت سرور و زمان پاسخ سریع (تجارت الکترونیکی، چت آنلاین و ...) مدیریت می‌کنند، مناسب است.

چرا باید از NestJS استفاده کنیم؟

مزایای استفاده از NestJS بی شمار است. مهمترین دلایل عبارتند از:

  • Type checking: با Typescript ما از تمام قدرت جاوااسکریپت بدون استفاده از مشکلات نوع استفاده می‌کنیم.
  • Modular: از طریق تقسیم به ماژول‌ها می‌توانیم اجزای خود را بر اساس مرز دامنه جدا کنیم.
  • Dependency Injection: به لطف کانتینر DI ما قادر به نوشتن کد جدا شده که قابل تست می‌باشد.
  • Adapters: Nest تمام کتابخانه‌های javascript / typescript را که بیشتر مورد استفاده و آزمایش قرار گرفته است، ادغام می‌کند و تمام ابزارهایی را که توسعه دهندگان برای کمک به مجموعه نیاز دارند فراهم می‌کند.
  • Angular Like: این ساختار به ما امکان می‌دهد تا بدون نیاز به تغییر روش توسعه، از فرانت-اند به بک-اند سویچ کنیم.
  • CLI: می‌توانیم عملکرد خود را بدون نگرانی در مورد ساختار اولیه توسعه دهیم.

بیایید شروع به نوشتن کد کنیم

  • بازیابی لیست همه todo ها
  • بازیابی یک todo
  • اضافه کردن todo
  • ویرایش todo
  • حذف todo

بنابراین از API های زیر استفاده می‌کنیم:

  • [GET] todos
  • [GET] todos/{id}
  • [POST] todos
  • [PUT] todos/{id}
  • [DELETE] todos/{id}

قدم صفر: نصب

راه‌اندازی پروژه با دو دستور ساده زیر در ترمینال انجام می‌شود:

npm i -g @nestjs/cli
nest new TodoBackend

npm run start را در پوشه todo-backend اجرا کنید تا مطمئن شوید همه چیز به خوبی کار می‌کند.

قدم اول: ماژول

ماژول مکانیزمی است که به ما امکان می‌دهد اجزای خود را بر اساس دامنه‌ای که به آن تعلق دارند جدا کنیم و در عمل کانتینر DI را در تعاملات آنها در مرحله راه‌انداز به کار گیریم.

ماژول اصلی که بوت استرپ با آن انجام می‌شود، ماژول root نامیده می‌شود و در برنامه‌های تولید شده از طریق CLI، آن را در پوشه src با نام AppModule پیدا می‌کنیم.

برنامه ما بسیار کوچک و با یک قابلیت واحد است که می‌تواند به طور مستقیم از ماژول root برای مدیریت وابستگی‌های خود استفاده کند. حداقل دو دلیل وجود دارد که ما این کار را در این مقاله انجام نمی‌دهیم:

  • این یک مورد معمولی نیست و در نتیجه کاربرد آن در یک زمینه واقعی دشوار است.
  • قابلیت نگهداری و قابلیت حمل کد خود را از دست می‌دهیم.

بنابراین ماژول خود را از طریق CLI ایجاد می‌کنیم:

nest generate module todo
@Module({
  imports: [],
  providers: [],
  controllers: []
})
export class TodoModule {}

هنگام تولید آن، CLI همچنین با وارد کردن TodoModule به عنوان یک ماژول ویژگی، از به روزرسانی AppModule مراقبت خواهد کرد.

قدم دوم: موجودیت

موجودیت یک کلاس است که یک جدول (یا مجموعه) از پایگاه داده را ترسیم می‌کند (تعریف بسیار عملی).

ما موجودیت خود را از طریق CLI ایجاد می‌کنیم:

nest generate class todo/entities/Todo --no-spec
export class Todo {
  
  public id: number;
  public title: string;
  public completed: boolean;

  public constructor(title: string) {
    this.title = title;
    this.completed = false;
  }

}

قدم سوم: ریپازیتوری

اکنون که موجودیت خود را داریم، فقط باید آن را از طریق ORM ادامه دهیم.

برای این کار تصمیم گرفته‌ایم از Typeorm و راه‌اندازی یک ارتباط اساسی با پایگاه داده sqlite استفاده کنیم.

ابتدا وابستگی‌ها را نصب می‌کنیم:

npm i @nestjs/typeorm typeorm sqlite3

AppModule را با وارد کردن TypeOrmModule با استفاده از متد استاتیک forRoot اصلاح می‌کنیم و سپس تنظیمات مورد نیاز را انجام می‌دهیم:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as path from 'path';
import { TodoModule } from './todo/todo.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      autoLoadEntities: true,
      synchronize: true,
      database: path.resolve(__dirname, '..', 'db.sqlite')
    }),
    TodoModule
  ]
})
export class AppModule {}

بیایید TypeOrmModule را نیز در TodoModule اضافه کنیم، این بار با استفاده از متدforFeature ، Todo را به عنوان موجودیتی برای مدیریت مشخص می‌کنیم:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './entities';

@Module({
  imports: [
    TypeOrmModule.forFeature([Todo])
  ],
  providers: [],
  controllers: []
})
export class TodoModule {}

اکنون که Typeorm را پیکربندی کردیم، سرانجام میتوانیم موجودیت Todo خود را با تمام حاشیه نویسیهای لازم به روز کنیم:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Todo {

  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public title: string;

  @Column()
  public completed: boolean;

  public constructor(title: string) {
    this.title = title;
    this.completed = false;
  }
}

می‌توانید اطلاعات بیشتر در مورد Typeorm و حاشیه نویسی‌های آن را با مراجعه به لینک پیوست شده در ابتدای مرحله بخوانید.

برای متد TypeOrmModule forRoot و forFeature، می‌توانید از بخش پایگاه داده در مستندات رسمی NestJS کمک بگیرید: https://docs.nestjs.com/techniques/database

قدم چهارم: DTO

برای جلوگیری از افشای موجودیت‌های خود در خارج از لایه منطق، مجموعه‌ای از کلاس‌ها را تعریف می‌کنیم که برای مدیریت ارتباطات داخل و خارج از خدمات استفاده می‌شود: DTO (اشیا انتقال داده).

export class AddTodoDto {

  public readonly title: string;

  public constructor(opts?: Partial<AddTodoDto>) {
    Object.assign(this, opts);
  }

}
export class EditTodoDto {

  public readonly title: string;
  public readonly completed: boolean;

  public constructor(opts?: Partial<EditTodoDto>) {
    Object.assign(this, opts);
  }

}
export class TodoDto {

  public readonly id: number;
  public readonly title: string;
  public readonly completed: boolean;

  public constructor(opts?: Partial<TodoDto>) {
    Object.assign(this, opts);
  }

}

قدم پنجم: سرویس

سرویس پکیجی است که ما قصد داریم منطق کار خود را در آن جمع کنیم و مجموعه‌ای از ویژگی‌های آماده برای استفاده را در معرض دید قرار دهیم.

بیایید اشیایی را تعریف کنیم که لایه سرویس ما را پر کنند:

nest generate service todo/services/todo

در سرویس ایجاد شده متدهایfindAll ، findOne ، add، edit و delete را که از طریق DTO ها توسط کنترل کننده مصرف می‌شود، پیاده سازی می‌کنیم.

برای جدا کردن منطق تبدیل از Entity به DTO (و بالعکس)، بیایید یک TodoMapperService ایجاد کنیم:

nest generate service todo/services/TodoMapper
import { Injectable } from '@nestjs/common';
import { Todo } from '../../entities';
import { TodoDto } from '../../dto';

@Injectable()
export class TodoMapperService {

  public modelToDto({ id, title, completed }: Todo): TodoDto {
    return new TodoDto({ id, title, completed });
  }

}

اکنون بیایید TodoService خود را پیاده سازی کنیم. ما از طریق وابستگی‌ها ریپازیتوری Todo را که توسط Typeorm و TodoMapperService ارائه شده تزریق می‌کنیم:

import { isNullOrUndefined } from 'util';
import { Injectable, NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Todo } from '../../entities';
import { TodoDto, AddTodoDto, EditTodoDto } from '../../dto';
import { TodoMapperService } from '../todo-mapper/todo-mapper.service';

@Injectable()
export class TodoService {

  public constructor(
    @InjectRepository(Todo) private readonly todoRepository: Repository<Todo>,
    private readonly todoMapper: TodoMapperService
  ) {}

  public async findAll(): Promise<TodoDto[]> {
    const todos = await this.todoRepository.find();
    return todos.map(this.todoMapper.modelToDto);
  }

  public async findOne(id: number): Promise<TodoDto> {
    const todo = await this.todoRepository.findOne(id);
    if (isNullOrUndefined(todo)) throw new NotFoundException();
    return this.todoMapper.modelToDto(todo);
  }


  public async add({ title }: AddTodoDto): Promise<TodoDto> {
    let todo = new Todo(title);
    todo = await this.todoRepository.save(todo);
    return this.todoMapper.modelToDto(todo);
  }

  public async edit(id: number, { title, completed }: EditTodoDto): Promise<TodoDto> {
    let todo = await this.todoRepository.findOne(id);

    if (isNullOrUndefined(todo)) throw new NotFoundException();

    todo.completed = completed;
    todo.title = title;

    todo = await this.todoRepository.save(todo);

    return this.todoMapper.modelToDto(todo);
  }

  public async remove(id: number): Promise<Todo> {
    let todo = await this.todoRepository.findOne(id);

    if (isNullOrUndefined(todo)) throw new NotFoundException();

    todo = await this.todoRepository.remove(todo);

    return todo;
  }

قدم ششم: کنترلر

در اینجا ما در آخرین لایه صعود به پشته NestJS هستیم.

برای ایجاد کنترلر با استفاده از این دستور از CLI خود برای آخرین بار استفاده خواهیم کرد:

nest generate controller todo/controllers/todo

بیایید بقیه متدهایی را که در ابتدای مقاله ذکر کردیم پیاده سازی کنیم و آنها را به متدهای TodoService قلاب (hook) کنیم:

import { TodoService } from './../services/todo/todo.service';
import { TodoDto, AddTodoDto, EditTodoDto } from './../dto';

import {
  Controller, 
  Get,
  Param,
  Post,
  Put,
  Body,
  Delete
} from '@nestjs/common';


@Controller('todos')
export class TodoController {

  public constructor(private readonly todoService: TodoService) {}

  @Get()
  public findAll(): Promise<TodoDto[]> {
    return this.todoService.findAll();
  }

  @Get(':id')
  public findOne(@Param('id') id: number): Promise<TodoDto> {
      return this.todoService.findOne(id);
  }

  @Put(':id')
  public edit(@Param('id') id: number, @Body() todo: EditTodoDto): Promise<TodoDto> {
      return this.todoService.edit(id, todo);
  }

  @Post()
  public add(@Body() todo: AddTodoDto): Promise<TodoDto> {
      return this.todoService.add(todo);
  }

  @Delete(':id')
  public remove(@Param('id') id: number): Promise<TodoDto> {
      return this.todoService.remove(id);
  }

}

هشدار: سریال سازی DTO فعال نیست مگر اینکه متد کنترل کننده خود را با ClassSerializerInterceptor تزئین کنید.

@Post()
@UseInterceptors(ClassSerializerInterceptor)
public add(@Body() todo: AddTodoDto): Promise<TodoDto> {

در مرحله بعدی با توسعه راه‌حلی که به ما امکان می‌دهد جداکننده را متمرکز کنیم، این موضوع را بررسی می‌کنیم.

قدم هفتم: اعتبارسنجی

DTO های ما آماده هستند تا به سرعت تحت پروتکل http حرکت کنند، اما قطعه آخر وجود ندارد: اعتبار سنجی داده‌های آن.

برای مدیریت اعتبار فیلدها، NestJS یک متد اعتبار سنجی ارائه می‌دهد که از کتابخانه‌های class-transformer و class-validator استفاده می‌کند. برای اینکه بتوانیم آن را به کار بگیریم، باید وابستگی‌های آن را در پروژه نصب کنیم:

npm i class-transformer class-validator

بیایید ValidationPipe را به global pipe اضافه کنیم:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  await app.listen(3000);
}
bootstrap();

و حالا بیایید DTO های خود را تزئین کنیم:

import { IsNotEmpty } from 'class-validator';

export class EditTodoDto {

  @IsNotEmpty()
  public readonly title: string;

  public readonly completed: boolean;

  public constructor(opts?: Partial<EditTodoDto>) {
    Object.assign(this, opts);
  }

}

هشدار: پس از اینکه برنامه ما کامپایل شد، تمام DTO هایی که تاکنون تعریف کرده‌ایم به اشیا جاوااسکریپت تبدیل می‌شوند. این بدان معنی است که بررسی مقادیر فیلدهای آن انجام نمی‌شود!

بنابراین آیا اعتبارسنج‌های ما فقط تا زمانی که از مقادیر نوع صحیح عبور کرده باشند کار می‌کنند؟ جواب منفی است.

کتابخانه class-validator همچنین دارای مجموعه اعتبارسنج‌هایی است که به طور خاص برای تایپ کردن فیلدهای ما در زمان اجرا طراحی شده‌اند:

import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';

export class EditTodoDto {

  @IsString()
  @IsNotEmpty()
  public readonly title: string;

  @IsBoolean()
  public readonly completed: boolean;

  public constructor(opts?: Partial<EditTodoDto>) {
    Object.assign(this, opts);
  }

}

قدم هشتم: اجرا

زمان اجرای برنامه فرا رسیده است.

برای اجرای معمول آن، فقط دستور زیر را اجرا کنید:

npm run start

اگر لازم است کد خود را دیباگ کنیم، باید دستور زیر را اجرا کنیم:

npm run start:debug

قدم نهم: به کارگیری CORS

پس از توسعه بک-اند، اگر بخواهیم فرانت-اند را توسعه دهیم، در اینجا CORS وارد عمل می‌شود:

Cross-Origin Resource Sharing (CORS) مکانیزمی است که با استفاده از هدرهای اضافی HTTP به مرورگرها می‌گوید که به یک برنامه وب با یک منبع، دسترسی به منابع انتخاب شده از مبدأ دیگر را بدهند. به دلایل امنیتی، مرورگرها درخواست‌های متناوب HTTP را که از اسکریپتها شروع شده‌اند، محدود می‌کنند. به عنوان مثال XmlHttpRequest و Fetch API از همین خط مشی پیروی می‌کنند.

https://developer.mozilla.org/en/docs/Web/HTTP/CORS

برای فعال کردن CORS کافیست با فراخوانی متد ()enableCors با تمام پارامترهای پیکربندی مورد نیاز، main.ts را دوباره ویرایش کنید. برای سادگی کار همه چیز را فعال خواهیم کرد.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  app.enableCors();

  await app.listen(3000);
}
bootstrap();

قدم دهم: تست

تست برنامه کاملا ضروری است. درصورتی که بخواهید یک برنامه قابل نگهداری بسازید، این به دلیل تغییر عملکرد موجود، از رایج‌ترین خطاهای توسعه و رگرسیون‌های احتمالی جلوگیری می‌کنند.

وقتی ما یک مولفه جدید از طریق CLI تولید می‌کنیم، فایل آزمایشی آن نیز به طور خودکار تولید می‌شود. بیایید سعی کنیم یکی از آنها را بسازیم:

import { Test, TestingModule } from '@nestjs/testing';
import { TodoService } from './todo.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Todo } from './../../entities';
import { repositoryMockFactory, MockType } from './../../../utils/test/repository.mock';
import { TodoMapperService } from './../todo-mapper/todo-mapper.service';
import { Repository } from 'typeorm';

describe('TodoService', () => {
  let service: TodoService;
  let repository: MockType<Repository<Todo>>;
  
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TodoService,
        TodoMapperService,
        { provide: getRepositoryToken(Todo), useFactory: repositoryMockFactory }
      ],
    }).compile();
    repository = module.get<Repository<Todo>>(getRepositoryToken(Todo)) as unknown as MockType<Repository<Todo>>;
    service = module.get<TodoService>(TodoService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should throws exception when todo not exists', async () => {
    repository.findOne.mockReturnValue(Promise.resolve(null));
    await expect(service.findOne(1)).rejects.toThrow('Not Found');
  });

});

برای تست، Nest از Jest استفاده می‌کند.

برای اطلاعات بیشتر به مستندات NestJS به آدرس https://docs.nestjs.com/fundamentals/testing مراجعه کنید.

جمع‌بندی

با ایجاد اپلیکیشنی که در این مقاله توضیح داده شد، از نزدیک دیدیم که این فریمورک چقدر سریع و قدرتمند است و به شما امکان می‌دهد بسیار سریع و انعطاف‌پذیر باشید بدون اینکه مطلقا از هر چیزی صرف نظر کنید. NestJS امتحانات خود را پشت سر گذاشته و می‌تواند جایگاهی را در لیست برترین فریمورک‌های توسعه وب به خود اختصاص دهد.

منبع

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
عرفان حشمتی @heshmati74
مهندس معماری سیستم های کامپیوتری، طراح و توسعه دهنده وب سایت
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو