Open9

NestJS + Next.js

shiroshiro

NestJS is 何?

Nest (NestJS)は、効率的でスケーラブルなNode.jsサーバーサイドアプリケーションを構築するためのフレームワーク

NestJSのプロジェクト作成

  • 以下コマンドを実行してプロジェクトを作成する
$ npm i -g @nestjs/cli
$ nest new project-name
  • 作成後、tsconfig.jsonのstrictをtrueにする
  • main.tsのポート番号を変更する
    3000番はReactで使用するため3005に変更
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);
+   await app.listen(3005);
}
bootstrap();
shiroshiro

NestJSの全体的な構成について

NextJSでは認証関連はauth.module、タスク関連はtask.moduleというように各機能をモジュールという単位で開発する。
各モジュールをルートモジュールであるapp.moduleにインポートすることでプロジェクト内で使えるようになる。

src
├─ app.controller.spec.ts // controllerのユニットテスト
├─ app.controller.ts // ルーティング処理
├─ app.module.ts // アプリケーションのルートモジュール
├─ app.service.ts // ビジネスロジック
└─ main.ts // アプリケーションのコードが実行されるときのエントリーポイント
shiroshiro

DI(Dependency Injection) is 何?

Dependency Injectionとは疎結合にするためのデザインパターンの一つ

app.controllerを例に詳しく見る

やりたいこと

app.controllerでapp.serviceに定義されたgetHello()メソッドを使いたい。

具体例

  • app.serviceにはgetHello()メソッドの具体的なロジックが書かれている。
  • ここでAppServiceクラスがInjectable()というデコレータで装飾されていることで他のコントローラーやサービスにインジェクション注入できるようになる。
app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

DI(Dependency Injection)

  • 通常だとapp.controllerの中でAppServiceをインスタンス化して、インスタンス.getHello()という形で取得することが考えられる
    → これだとコントローラーとサービスの依存関係が強くなってしまう

  • DIを使うことでコントローラー内の実装でAppServiceをインスタンス化しないというのが1つのポイント
    → インスタンス化されたものをコントローラーのconstructor経由で注入する

  • AppControllerのconstructorに注入したいサービスを指定する必要がある。
    AppControllerがインスタンス化されるときにAppServiceもインスタンス化してくれる。
    → インスタンス化されたものはappServiceという名前で利用できる。
    AppServiceのインスタンスはconstructor経由でAppControllerのインスタンスに注入される

app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

constructorでサービスを自動的にインスタンス化してコントローラーにインジェクションしてくれるという処理はNestJSの中にあるIoC Containerが裏側ですべてやってくれている。

app.moduleの設定

  • app.moduleの中でAppControllerを使うためにcontrollers:[]AppControllerを指定する。
  • AppControllerの中でAppServiceを使うために providers: []AppServiceを指定する。
app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

shiroshiro

コントローラーのルートパスについて

@Controller()に何も指定していないので、このGETメソッドはルートパスに対応したものとなる。

app.controller.ts
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

ブラウザで呼び出し

http://localhost:3005/へアクセスすることでHello Worldというテキストがレスポンスで返ってきていることが確認できる。

@Controller("root")に変更

@Controller("root")と変更すると対応したパスが変わるためhttp://localhost:3005/へアクセスするとエラーが返ってくる。

http://localhost:3005/rootでアクセスすることでHello Worldが返ってくる。

個別のメソッドに対してパスを指定することも可能

@Get()に対してパスを指定することも可能。こうすることでroot/nextのエンドポイントに対応する。

app.controller.ts
  @Get('next')
  getHello(): string {
    return this.appService.getHello();
  }

http://localhost:3005/root/next

shiroshiro

NestJSでのDI

NestJSでのDIは2パターン

パターン1

  1. InjectionしたいServiceに@Injectionデコレータを付ける
@Injectable()
export class AuthService {
  1. Serviceを使用したいModuleのprovidersに登録
@Module({
  imports: [],
  controllers: [AuthController],
  providers: [AuthService ],
})
  1. ControllerのコンストラクタにServiceを追加
@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

パターン2

  1. InjectionしたいServiceに@Injectionデコレータを付ける
prisma.service
@Injectable()
export class PrismaService {
prisma.module
@Module({
  providers: [PrismaService],
  exports: [PrismaService], // exportに追加するとModule importでServiceも使用可能になる
})
  1. Serviceを使用したいModuleのimportsに外部Module登録
auth.module
@Module({
  imports: [PrismaModule],
  1. Serviceのコンストラクタに外部Serviceを追加
auth.service
@Injectable()
export class AuthService {
  constructor(
    private readonly prisma: PrismaService, // IoC Containerが依存関係を判定して自動的にインスタンス化してくれる

JWTのソースコードもパターン2の形で実装されている。

jwt.module.ts
@Module({
  providers: [JwtService],
  exports: [JwtService]
})

https://github.com/nestjs/jwt/tree/master/lib

shiroshiro

Prismaのセットアップ

  • prismaをインストール
yarn add -D prisma
  • @prisma/clientをインストール
yarn add @prisma/client
  • prisma生成
npx prisma init

dockerファイル作成

ルートにdocker-compone.ymlファイルを作成する。

docker-compone.yml
version: '3.8' # Docker Composeファイルのバージョンが3.8であることを示す
services: # 起動するサービスの定義
  dev-postgres: # サービスの名前
    image: postgres:14.4-alpine # PostgreSQLの14.4-alpineイメージを使用してコンテナを起動
    ports: # ホストマシンのポート5434をコンテナのポート5432とマッピング
      - 5434:5432
    environment: #  PostgreSQLコンテナ内で使用される環境変数の定義。ユーザ名、パスワード、およびデータベース名を設定
      POSTGRES_USER: nest
      POSTGRES_PASSWORD: nest
      POSTGRES_DB: nest
    restart: always # コンテナがクラッシュした場合に自動的に再起動
    networks: # コンテナが参加するネットワークの定義。lessonという名前のネットワークに参加。
      - lesson
networks: # 定義されたネットワークのリスト。単一のネットワークlessonが定義されている
  lesson:

データベース起動

以下コマンドを実行してバックグラウンドでコンテナを立ち上げる。

docker compose up -d

model構造を定義

schema.prismaファイルにUser、Taskのモデルを定義する

schema.prisma
model User {
  id Int @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  email String  @unique // 重複を許可しない
  hashedPassword String //hash化されたPWを保存
  tasks Task[]
}

model Task {
  id Int @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title String
  description String? // 任意
  userId Int
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
  • モデルを定義したら以下コマンドを実行してpostgresqlのデータベースに反映させる
npx prisma migrate dev
  • 以下コマンドを実行することで実際のデータベースの構造を見ることができる。
npx prisma studio

  • 以下コマンドを実行することでモデル構造から型定義を自動生成できる
npx prisma generate
shiroshiro

必要なパッケージをインストール

以下コマンドを実行

yarn add @nestjs/config @nestjs/jwt @nestjs/passport 
yarn add cookie-parser csurf passport passport-jwt bcrypt class-validator
yarn add -D @types/express @types/cookie-parser @types/csurf 

※Windows環境でパッケージをインストールしようとしたところエラーが発生してインストールできない事象が発生。
開発サーバーを停止して再度試したところエラーなくインストールできた。
https://stackoverflow.com/questions/64603970/an-unexpected-error-occurred-eperm-operation-not-permitted-in-yarn

shiroshiro

Module、Controller、Serviceファイル作成

以下コマンドを実行してModule、Controller、Serviceファイルを作成する

nest g module xxxx
nest g controller xxxx --no-spec // --no-specとすることでテストファイルを生成しない
nest g service xxxx --no-spec // --no-specとすることでテストファイルを生成しない

コマンド実行で作成するとapp.moduleのインポートが自動で更新される。

app.module
@Module({
  imports: [AuthModule, UserModule, TodoModule, PrismaModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
shiroshiro

環境変数について

環境変数をプロジェクト全体で使用するためにapp.module.tsに環境変数用のconfigを追加する

app.module.ts
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    // isGlobal: trueとすることで個別のモジュールでインポートする必要がなくなる
    ConfigModule.forRoot({ isGlobal: true }), 
    AuthModule,
    UserModule,
    TodoModule,
    PrismaModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

main.ts

必要なパッケージを追加

main.ts
// DTOでクラスバリデーションを有効化のために必要
import { ValidationPipe } from '@nestjs/common'; 
// Requestのデータ型
import { Request } from 'express';
// クライアントのリクエストからCookieを取り出すためのパーサー
import * as cookieParser from 'cookie-parser'; 
// CSRF対策
import * as csurf from 'csurf'; 

bootstrap()に設定追加

main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
  app.enableCors({
    credentials: true,
    origin: ['http://localhost:3000'],
  });
  app.use(cookieParser());
  app.use(
    csurf({
      cookie: {
        httpOnly: true,
        sameSite: 'none',
        secure: true,
      },
      value: (req: Request) => {
        return req.header('csrf-token');
      },
    }),
  );
  await app.listen(process.env.PORT || 3005);
}
bootstrap();
  • credentialsをfalseに設定すると、クロスオリジンリクエストで送信されるCookieがサーバーによって無視され、サーバー側はクライアントから送信されるCookieを受け取らず、JWTトークンを含む情報を取得することができない。