Nestjs入門

komosyu
Nestjs

目次

  1. はじめに
  2. Nestjs について軽く
  3. 技術スタック
  4. Nestjs の基礎概念
    1. コマンドライン
  5. ざっくり構成
  6. CRUD 操作をやりながらの説明
    1. Module
    2. ちょっと待って
    3. Entity
    4. Dto
    5. Repository
    6. Controller
    7. Service
  7. 参考
  8. さいごに

はじめに

最近事業会社の方に転職しまして、これまで制作会社でフロントエンド周りしかやってこなかったのですが、転職してからは大きい会社ではないしエンジニアも少ないのでバックエンド周りも触るようになってきました。(個人的にも個人開発でひとりですべて手掛けたいという気持ちがあったのでありがたい)

それでも別にエンジニアの適性もないしいきなり未知のバックエンド(特に日本語情報の少ない Nestjs)を習得するのは気が遠くなるなと思っていたところ、Udemy で非常にわかりやすくて楽しい講座に遭遇できたので、そこで学んだ中の一部の CRUD 操作など基礎的なところをコースの紹介がてらしていこうと思います!

Nestjs について軽く

まずは Nestjs とは?というところを軽く触れていこうと思います。

もちろん詳細はぜひドキュメントを読んでみて欲しいところではありますが。

Nestjs は Node.js 製のフレームワークで TypeScript なのでフロントエンドで TypeScript を使いたい時に型の整合性がとれるのがよくて人気というのと Angular とよく似た設計をしているというのが有名ですね。
Angular をまったく触れたことがないのでその辺はよくわかりませんが...

それと、メルカリや Ubie といった人気企業でも採用されているので近年の注目度は高まってきているのではないでしょうか。

技術スタック

今回の Udemy の講座で使われている技術スタックはこんな感じでした。

  • Nestjs
  • TypeORM
  • pgAdmin
  • Docker

今回触れるのは Nestjs と TypeORM なので、細かいところはぜひコースを受講してみてください。

Nestjs の基礎概念

コマンドライン

これから Nestjs の基礎概念を掴んでいくのにコマンドラインを抑えておく必要があります。

何はともあれグローバルインストール

npm i -g @nestjs/cli

そしたら Nestjs のプロジェクトを作成したい場所に入って以下のコマンドでプロジェクトをつくっていきます。

nest new project-name

これで Nestjs のプロジェクトがセットされました。

readme にも書いてあると思いますが、このコマンドでサーバーが立ち上がります。

yarn start:dev

ざっくり構成

私なりの理解ですがプロジェクトの中身はざっくりとこんな感じになっています。

  • アプリケーション => main.ts
  • ルートモジュール => app.module.ts
  • 機能モジュール => feature.module.ts

アプリケーションがサーバーの基盤となっているようなところでルートモジュールであるapp.module.tsを読み込んでいます。

それで、そのapp.module.tsでは認証だとか例えば商品の CRUD 処理だとか、それぞれの機能を持ったfeature.module.tsをまとめて読み込んでいるような場所になっています。

いや、module ってなんだよとかって話になってくると思うので、ここからはいわゆる機能ごとの中身はどうなっているのかの構造を見ていきたいと思います。

大きく分けて 3 つに役割があります。

  • Module
  • Controller
  • Service

それでは以下でそれぞれ触れていきましょう。

CRUD 操作をやりながらの説明

Module

Module はそれぞれの機能単位の中で Controller と Service を紐づけるもので、それを最終的にはapp.module.tsに読み込ませて動くようにしていきます。

Module をつくる時にも Nestjs のコマンドラインを活用していきます。

Nestjs では Module だけでなく以下のコマンドを活用してファイルの生成をすることができます。

nest g <schematic> <name> [options]

今回のような機能 Module の場合はこんなコマンドを打ち込みます。
例えば投稿機能でpostsという Module(ディレクトリごと)をつくってみましょうか。

nest g module posts

これで/src/posts/posts.module.tsというファイルがつくられました。

中身はというとこんな感じになっています。

import { Module } from '@nestjs/common'

@Module({
  // この中でController,Serviceを読み込む
  controllers: [] //Controller
  providers: [] //Service
})[]
export class PostsModule {}

ちょっと待って

たしかに事前に説明していたように Nestjs のひとつの機能単位では(Module, Controller, Service)でよかったんだけれども、DB が絡んでくるともうちょっとだけ用意しておくべきものがありました。

  • Entity
  • Dto
  • Repository

説明していきますね。

Entity

Entity は DB に組み込まれるテーブルの構造を定義しておくことができるものです。

/src/entities/posts.entity.ts

といった感じで entity をつくっておきましょう。

import { PostStatus } from 'src/posts/post-status.enum';
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity() // Entityデコレーターを付けておきます

// DBにこういうテーブルができるイメージ
export class Post = {
    @PrimaryGeneratedColumn("uuid")
    id: string

    @Column()
    title: string

    @Column()
    content: string

    @Column()
    status: PostStatus
}

まず、今回の投稿でつくりたいのは以下のものです。

  • 投稿ごとのオリジナルの id
  • タイトル
  • 内容
  • 公開/非公開の状態

それぞれに TypeORM のデコレーターを付けてあげる必要があるのですが、そもそも ORM というのはプログラムと DB との間のマッピングを行なってくれるものでプログラム側でこれから DB にどんなデータが入ってくるのかを定義しておく必要があります。

@Columnはよく見かける一般的なデコレーターで通常のカラムを定義するものです。
今回は必要ありませんが、引数でデータ型や長さ、ユニークかなものかなどを指定することもできます。

@PrimaryGeneratedColumnは自動的に生成されるカラムを定義します。
DB に行が追加されるたびに自動的にインクリメントされるようになっているようですね。

あと、投稿の公開/非公開の状態を持っておくのにPostStatusを用意しておきましょう。

/src/posts/post-status.enumに 2 つの状態を用意しておきます。

export enum PostsStatus {
  RELEASE = 'RELEASE',
  DRAFT = 'DRAFT',
}

Dto

Dto は何かというとData Transfer Objectの 略称で文字通りデータのやり取りをする際に使われるオブジェクト構造を定義しておくことができるのです。

/src/posts/dto/create-posts.dto.ts

みたいな感じで dto をつくっておきましょう。

  • メンテナンス性
  • 安全性
  • バリデーション機能

ここを定義しておくことであらかじめ入ってくる値を制御することができるので上記のメリットを享受できるのですね。

import { Type } from 'class-transformer'
import { IsInt, IsNotEmpty, IsString, MaxLength, Min } from 'class-validator'

export class CreatePostDto {
  // こんなんな感じでバリデーションをかけられる
  @IsString()
  @IsNotEmpty()
  @MaxLength(40)
  title: string

  @IsString()
  @Min(10)
  content: number
}

Repository

Repository は先ほど作成した Entity を使って DB を操作するもので、Entity と Repository を 1:1 として DB 操作を抽象化することができるようになるのです。

Repository を使うことで DB 操作を一箇所で行うことができてコードの再利用性を高めることもできるようになります。

/src/posts/posts.repository.ts

といった感じで Repository をつくっておきましょう。

import { Post } from 'src/entities/post.entity'
import { EntityRepository, Repository } from 'typeorm'
import { CreatePostDto } from './dto/create-post.dto'
import { PostStatus } from './post-status.enum'

@EntityRepository(Post) // EntityRepositoryデコレーターを付けておきます/引数には作成しておいたEntityを
export class PostRepository extends Repository<Post> {
  async createPost(createPostDto: CreatePostDto): Promise<Post> {
    // 作成しておいたCreatePostDtoから値を取り出す
    const { title, content } = createPostDto
    // TypeORMのcreateメソッドでpost.entityをインスタンスに変換します
    const post = this.create({
      title,
      content,
      status: PostStatus.RELEASE,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    })

    // createでは作成しただけでDBには保存されないのでここでDB上に保存
    await this.save(post)

    return post
  }
}

Controller

Controller はルーティングを担当するファイルです。

アプリケーションに対して特定のリクエストを受信することができます。

例えば先ほどの Module でpostsというディレクトリを作成したので、そこでルーティングを担当する Controller をつくっていきましょう。
ここでhttp://localhost:3000/postsみたいな url ができあがるのですが、この中でhttp://localhost:3000/posts/1みたいな感じで複数のルーティングをつくることができるのです。

nest g controller posts

これで/src/posts/posts.controller.tsというファイルができました。

import { Controller } from '@nestjs/common'

@Controller('posts') // ex)http://localhost:3000/posts
export class PostsController {}

さて、それではクラスの中にルーティングの処理を書いていきましょう。

今回は簡単な CRUD アプリケーションなので、http メソッドのget/post/patch/deleteができるようにしていきますよ。

こんな感じになります。

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common'
import { PostsService } from './posts.service'
import { CreatePostDto } from './dto/create-post.dto'
import { Post } from 'src/entities/post.entity'

@Controller('posts') // ex)http://localhost:3000/posts
export class PostsController {
  // http://localhost:3000/postsですべての投稿を取得
  @Get()
  async findAll(): Promise<Post[]> {
    return await this.postsService.findAll()
  }

  // http://localhost:3000/posts/1,2みたいな感じで引数によって特定の投稿を取得
  @Get(':id')
  async findById(id: string): Promise<Post> {
    return await this.postsService.findById(id)
  }

  // http://localhost:3000/postsで投稿
  @Post()
  // httpリクエストのBodyから取得したデータをcreatePostDtoに格納して
  async create(@Body() createPostDto: CreatePostDto): Promise<Post> {
    // Serviceのcreateメソッドに渡す
    return await this.postSercie.create(createPostDto)
  }

  // http://localhost:3000/posts/1,2みたいな感じで引数によって特定の投稿の状態を更新
  @Patch(':id')
  async updateStatus(id: string): Promise<Post> {
    return await this.postService.updateStatus()
  }

  // http://localhost:3000/posts/1,2みたいな感じで引数によって特定の投稿を削除
  @Delete(':id')
  async delete(id: string): Promise<void> {
    return await this.postService.delete()
  }
}

Controller はルーティングを行うので/post/1みたいにエンドポイントを定義して url にしたい引数を受け入れたりします。

ただ、それを使って具体的な処理を行うのは  Service になります。

Service

Service はビジネスロジックを書いていくファイルです。

Controller で作成されていたthis.postsService.findAll()にあったfindAllメソッド内で行う処理を書いていったりします。

nest g service posts

これで/src/posts/posts.service.tsというファイルができました。

ここで具体的なメソッドの内容を書いていきます。

import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common'
import { Post } from '../entities/post.entity'
import { CreatePostDto } from './dto/create-post.dto'
import { PostRepository } from './post.repository'
import { PostStatus } from './post-status.enum'

@Injectable()
export class PostsService {
  // 作成しておいたPostRepositoryを活用していくから楽ちん
  constructor(private readonly postRepository: PostRepository) {}

  async findAll(): Promise<Post[]> {
    // 全件投稿を取得する
    return await this.postRepository.find()
  }

  async findById(id: string): Promise<Post> {
    // 特定のidを持つ投稿を1件取得する
    const found = await this.postRepository.findOne(id)
    if (!found) {
      throw new NotFoundException(`投稿が見つかりません`)
    }
    return found
  }

  async create(createPostDto: CreatePostDto): Promise<Post> {
    // PostRepositoryで作成しておいたcreatePostで投稿を作成
    return await this.postRepository.createPost(createPostDto)
  }

  async updateStatus(id: string): Promise<Post> {
    // 更新したい投稿を1件取得しておく
    const post = await this.findById(id)
    // 状態を更新
    post.status = PostStatus.DRAFT
    // 更新した日時を設定
    post.updatedAt = new Date().toISOString()
    // DBに保存
    await this.postRepository.save(post)
    return post
  }

  async delete(id: string): Promise<void> {
    // 削除したい投稿を1件取得しておく
    const post = await this.findById(id)
    // DBから削除
    await this.postRepository.delete({ id })
  }
}

PostRepositoryではcreatePostメソッドしかつくっていなかったですが、TypeORM ではビルトインメソッドと呼ばれるものが用意されていて、それらを使って処理をしていくことができます。

たくさんありますが、シンプルな CRUD 処理ではこんなもので大丈夫です。
すべて追うのは難しいので、複雑なことをやろうとした時には TypeORM のドキュメントを見てみてください!

参考

さいごに

簡単にまとめようと思っていたのに結構なボリュームになってしまった...

DB を絡めると結構やることが多くて頭がこんがらがってしまいますね。

でもわかってくると結構楽しいものですな。

次はまた認証機能だとかもうちょっと複雑なことだとか実務でつまづいたところだとかを取り上げてみたいなーと思っております。