NestJS + Next.js

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に変更
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();

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 // アプリケーションのコードが実行されるときのエントリーポイント

DI(Dependency Injection) is 何?
Dependency Injectionとは疎結合にするためのデザインパターンの一つ
app.controllerを例に詳しく見る
やりたいこと
app.controllerでapp.serviceに定義されたgetHello()メソッドを使いたい。
具体例
- app.serviceにはgetHello()メソッドの具体的なロジックが書かれている。
- ここでAppServiceクラスが
Injectable()
というデコレータで装飾されていることで他のコントローラーやサービスにインジェクション注入できるようになる。
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
のインスタンスに注入される
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
を指定する。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

コントローラーのルートパスについて
@Controller()
に何も指定していないので、このGETメソッドはルートパスに対応したものとなる。
@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
のエンドポイントに対応する。
@Get('next')
getHello(): string {
return this.appService.getHello();
}
http://localhost:3005/root/next

NestJSでのDI
NestJSでのDIは2パターン
パターン1
- InjectionしたいServiceに@Injectionデコレータを付ける
@Injectable()
export class AuthService {
- Serviceを使用したいModuleのprovidersに登録
@Module({
imports: [],
controllers: [AuthController],
providers: [AuthService ],
})
- ControllerのコンストラクタにServiceを追加
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
パターン2
- InjectionしたいServiceに@Injectionデコレータを付ける
@Injectable()
export class PrismaService {
@Module({
providers: [PrismaService],
exports: [PrismaService], // exportに追加するとModule importでServiceも使用可能になる
})
- Serviceを使用したいModuleのimportsに外部Module登録
@Module({
imports: [PrismaModule],
- Serviceのコンストラクタに外部Serviceを追加
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService, // IoC Containerが依存関係を判定して自動的にインスタンス化してくれる
JWTのソースコードもパターン2の形で実装されている。
@Module({
providers: [JwtService],
exports: [JwtService]
})

Prismaのセットアップ
- prismaをインストール
yarn add -D prisma
- @prisma/clientをインストール
yarn add @prisma/client
- prisma生成
npx prisma init
dockerファイル作成
ルートに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のモデルを定義する
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

必要なパッケージをインストール
以下コマンドを実行
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環境でパッケージをインストールしようとしたところエラーが発生してインストールできない事象が発生。
開発サーバーを停止して再度試したところエラーなくインストールできた。

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のインポートが自動で更新される。
@Module({
imports: [AuthModule, UserModule, TodoModule, PrismaModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

環境変数について
環境変数をプロジェクト全体で使用するためにapp.module.tsに環境変数用のconfigを追加する
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
必要なパッケージを追加
// 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()に設定追加
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トークンを含む情報を取得することができない。