コンテンツにスキップ

NestJS

スケーラブルなサーバーサイドアプリケーションを構築するためのプログレッシブTypeScriptファーストNode.jsフレームワーク。

コマンド説明
npm i -g @nestjs/cliNestJS CLIをグローバルにインストール
nest new project-name新しいNestJSプロジェクトを作成
nest new project-name --strict厳密なTypeScriptでプロジェクトを作成
nest new project-name -p pnpmpnpmを使用してプロジェクトを作成
nest new project-name -p yarnYarnを使用してプロジェクトを作成
コマンド説明
npm run start:devホットリロード付きの開発サーバーを起動
npm run start:debugデバッグモードとホットリロードで起動
npm run start:prod本番サーバーを起動
npm run buildプロジェクトをビルド
npm run testユニットテストを実行
npm run test:watchウォッチモードでテストを実行
npm run test:covカバレッジレポート付きでテストを実行
npm run test:e2eエンドツーエンドテストを実行
npm run lintソースコードをlint
src/
├── app.controller.ts       # ルートコントローラー
├── app.controller.spec.ts  # コントローラーユニットテスト
├── app.module.ts           # ルートモジュール
├── app.service.ts          # ルートサービス
├── main.ts                 # アプリケーションエントリーポイント
├── users/
│   ├── users.module.ts     # 機能モジュール
│   ├── users.controller.ts # 機能コントローラー
│   ├── users.service.ts    # 機能サービス
│   ├── dto/
│   │   ├── create-user.dto.ts
│   │   └── update-user.dto.ts
│   ├── entities/
│   │   └── user.entity.ts
│   └── users.controller.spec.ts
├── auth/
│   ├── auth.module.ts
│   ├── auth.guard.ts
│   ├── auth.service.ts
│   └── strategies/
│       └── jwt.strategy.ts
└── common/
    ├── filters/
    │   └── http-exception.filter.ts
    ├── interceptors/
    │   └── logging.interceptor.ts
    └── pipes/
        └── validation.pipe.ts
コマンド説明
nest generate module users新しいモジュールを生成
nest generate controller users新しいコントローラーを生成
nest generate service users新しいサービスを生成
nest generate resource users完全なCRUDリソースを生成(モジュール + コントローラー + サービス + DTO)
nest generate guard auth認証ガードを生成
nest generate pipe validationバリデーションパイプを生成
nest generate interceptor loggingインターセプターを生成
nest generate middleware loggerミドルウェアを生成
nest generate filter http-exception例外フィルターを生成
nest generate decorator rolesカスタムデコレーターを生成
nest generate class user.entityプレーンクラスを生成
nest generate interface userインターフェースを生成
nest g res users --no-specテストファイルなしでリソースを生成
nest g mo users --flatサブフォルダーを作成せずに生成
nest infoプロジェクト情報と依存関係を表示
# 完全なCRUDリソース(最も一般的なワークフロー)
nest g resource users

# CLIプロンプト:
# ? What transport layer do you use? REST API
# ? Would you like to generate CRUD entry points? Yes

# 生成されるファイル:
# src/users/users.module.ts
# src/users/users.controller.ts
# src/users/users.service.ts
# src/users/dto/create-user.dto.ts
# src/users/dto/update-user.dto.ts
# src/users/entities/user.entity.ts
# src/users/users.controller.spec.ts
# 更新: src/app.module.ts(UsersModuleをインポート)
コマンド説明
@Module({ imports: [UsersModule] })別のモジュールをインポート
@Module({ controllers: [UsersController] })コントローラーを登録
@Module({ providers: [UsersService] })プロバイダー/サービスを登録
@Module({ exports: [UsersService] })他のモジュール用にプロバイダーをエクスポート
@Global()モジュールをグローバルに利用可能にする(控えめに使用)
import { Module, DynamicModule } from '@nestjs/common'

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      providers: [
        { provide: 'DATABASE_OPTIONS', useValue: options },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }

  static forRootAsync(options: DatabaseAsyncOptions): DynamicModule {
    return {
      module: DatabaseModule,
      global: true,
      imports: options.imports || [],
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject || [],
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    }
  }
}

// AppModuleでの使用
@Module({
  imports: [
    DatabaseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}
コマンド説明
@Controller('users')ルートプレフィックス付きのコントローラーを定義
@Get()GETリクエストを処理
@Post()POSTリクエストを処理
@Put(':id')パラメータ付きPUTリクエストを処理
@Patch(':id')PATCHリクエストを処理
@Delete(':id')DELETEリクエストを処理
@All('*')すべてのHTTPメソッドを処理
コマンド説明
@Param('id') id: stringルートパラメータを抽出
@Param('id', ParseIntPipe) id: number整数に抽出・パース
@Query('page') page: stringクエリパラメータを抽出
@Query() query: PaginationDtoすべてのクエリパラメータをDTOとして抽出
@Body() dto: CreateUserDtoリクエストボディを抽出・型付け
@Body('email') email: string特定のボディフィールドを抽出
@Headers('authorization') auth: stringリクエストヘッダーを抽出
@Ip() ip: stringクライアントIPアドレスを抽出
@Req() request: Request生のリクエストオブジェクトにアクセス
@Res() response: Response生のレスポンスオブジェクトにアクセス
@HttpCode(201)カスタムHTTPステータスコードを設定
@Header('Cache-Control', 'none')レスポンスヘッダーを設定
@Redirect('/new-url', 301)リダイレクトレスポンス
import {
  Controller, Get, Post, Put, Delete, Param, Query, Body,
  HttpCode, HttpStatus, ParseIntPipe, UseGuards, UseInterceptors,
} from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { JwtAuthGuard } from '../auth/jwt-auth.guard'
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor'

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

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto)
  }

  @Get()
  findAll(
    @Query('page', new ParseIntPipe({ optional: true })) page = 1,
    @Query('limit', new ParseIntPipe({ optional: true })) limit = 10,
  ) {
    return this.usersService.findAll(page, limit)
  }

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

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

  @Delete(':id')
  @UseGuards(JwtAuthGuard)
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}
コマンド説明
@Injectable()クラスをインジェクタブルプロバイダーとしてマーク
constructor(private usersService: UsersService)コンストラクタ経由でサービスをインジェクト
@Inject('TOKEN') private configカスタムトークンでインジェクト
{ provide: 'TOKEN', useValue: value }値プロバイダーを登録
{ provide: 'TOKEN', useFactory: () => ... }ファクトリプロバイダーを登録
{ provide: 'TOKEN', useClass: MyClass }クラスプロバイダーを登録
{ provide: 'TOKEN', useExisting: OtherService }既存プロバイダーのエイリアス
@Optional() private service?: MyServiceオプションの依存性注入
{ provide: 'TOKEN', scope: Scope.REQUEST }リクエストスコープのプロバイダー
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'

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

  async create(dto: CreateUserDto): Promise<User> {
    const exists = await this.usersRepo.findOneBy({ email: dto.email })
    if (exists) {
      throw new ConflictException('Email already registered')
    }
    const user = this.usersRepo.create(dto)
    return this.usersRepo.save(user)
  }

  async findAll(page: number, limit: number) {
    const [items, total] = await this.usersRepo.findAndCount({
      skip: (page - 1) * limit,
      take: limit,
      order: { createdAt: 'DESC' },
    })
    return { items, total, page, limit }
  }

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

  async update(id: number, dto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id)
    Object.assign(user, dto)
    return this.usersRepo.save(user)
  }

  async remove(id: number): Promise<void> {
    const result = await this.usersRepo.delete(id)
    if (result.affected === 0) {
      throw new NotFoundException(`User #${id} not found`)
    }
  }
}
npm install class-validator class-transformer
// main.ts - グローバルバリデーションを有効化
import { ValidationPipe } from '@nestjs/common'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,           // 不明なプロパティを除去
    forbidNonWhitelisted: true, // 不明なプロパティでエラー
    transform: true,           // ペイロードをDTO型に自動変換
    transformOptions: {
      enableImplicitConversion: true, // クエリ文字列を数値/ブール値に変換
    },
  }))
  await app.listen(3000)
}
import {
  IsString, IsEmail, IsOptional, IsInt, Min, Max,
  MinLength, MaxLength, IsEnum, ValidateNested, IsArray,
} from 'class-validator'
import { Type } from 'class-transformer'
import { PartialType, OmitType, PickType, IntersectionType } from '@nestjs/mapped-types'

export class CreateUserDto {
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string

  @IsEmail()
  email: string

  @IsString()
  @MinLength(8)
  password: string

  @IsOptional()
  @IsEnum(Role)
  role?: Role

  @IsOptional()
  @ValidateNested()
  @Type(() => AddressDto)
  address?: AddressDto
}

// 更新DTOを自動生成(すべてのフィールドがオプション)
export class UpdateUserDto extends PartialType(CreateUserDto) {}

// 特定のフィールドを選択
export class LoginDto extends PickType(CreateUserDto, ['email', 'password']) {}

// 特定のフィールドを除外
export class PublicUserDto extends OmitType(CreateUserDto, ['password']) {}
コマンド説明
@UseGuards(AuthGuard)コントローラーまたはルートにガードを適用
@UseGuards(AuthGuard, RolesGuard)複数のガードをチェーン
app.useGlobalGuards(new AuthGuard())グローバルガードを登録
canActivate(context: ExecutionContext)ガードロジックを実装
@SetMetadata('roles', ['admin'])ガード用にカスタムメタデータを設定
Reflectorガード内でメタデータを読み取り
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install -D @types/passport-jwt
// auth/auth.module.ts
@Module({
  imports: [
    UsersModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        secret: config.get('JWT_SECRET'),
        signOptions: { expiresIn: '1h' },
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

// auth/jwt.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(config: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: config.get('JWT_SECRET'),
    })
  }

  validate(payload: { sub: number; email: string }) {
    return { id: payload.sub, email: payload.email }
  }
}

// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// 使用例
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
  @Get()
  getProfile(@Req() req) {
    return req.user // JwtStrategy.validate()によりインジェクト
  }
}
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ])
    if (!requiredRoles) return true
    const { user } = context.switchToHttp().getRequest()
    return requiredRoles.some(role => user.roles?.includes(role))
  }
}

// 使用例
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
  @Get('dashboard')
  @Roles('admin')
  getDashboard() {
    return { message: 'Admin dashboard' }
  }

  @Delete('users/:id')
  @Roles('admin', 'superadmin')
  removeUser(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id)
  }
}

ミドルウェア、インターセプター、パイプ

Section titled “ミドルウェア、インターセプター、パイプ”
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now()
    res.on('finish', () => {
      const duration = Date.now() - start
      console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`)
    })
    next()
  }
}

// モジュールに登録
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*')
  }
}
import {
  Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common'
import { Observable, map, tap } from 'rxjs'

// レスポンス形状を変換
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, { data: T }> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<{ data: T }> {
    return next.handle().pipe(
      map(data => ({ data, timestamp: new Date().toISOString() })),
    )
  }
}

// 実行時間を計測
@Injectable()
export class TimingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const start = Date.now()
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start
        const response = context.switchToHttp().getResponse()
        response.setHeader('X-Response-Time', `${duration}ms`)
      }),
    )
  }
}
コマンド説明
npm install @nestjs/typeorm typeorm pgPostgreSQL付きTypeORMをインストール
TypeOrmModule.forRoot({ type: 'postgres', ... })AppModuleで接続を設定
TypeOrmModule.forFeature([User])機能モジュールにエンティティを登録
@InjectRepository(User) private repo: Repository<User>リポジトリをインジェクト
npm install @prisma/client
npm install -D prisma
npx prisma init
// prisma/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'
import { PrismaClient } from '@prisma/client'

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect()
  }

  async onModuleDestroy() {
    await this.$disconnect()
  }
}

// prisma/prisma.module.ts
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

// サービスでの使用
@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  findAll() {
    return this.prisma.user.findMany()
  }

  findOne(id: number) {
    return this.prisma.user.findUniqueOrThrow({ where: { id } })
  }
}
コマンド説明
throw new BadRequestException('Invalid input')400 Bad Request
throw new UnauthorizedException('Login required')401 Unauthorized
throw new ForbiddenException('Access denied')403 Forbidden
throw new NotFoundException('User not found')404 Not Found
throw new ConflictException('Email taken')409 Conflict
throw new UnprocessableEntityException('Validation failed')422 Unprocessable
throw new InternalServerErrorException()500 Internal Server Error
import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
} from '@nestjs/common'
import { Request, Response } from 'express'

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR

    const message = exception instanceof HttpException
      ? exception.getResponse()
      : 'Internal server error'

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: typeof message === 'string' ? message : (message as any).message,
    })
  }
}

// main.tsでグローバルに登録
app.useGlobalFilters(new AllExceptionsFilter())
npm install @nestjs/config
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.local', '.env'],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_URL: Joi.string().required(),
        JWT_SECRET: Joi.string().required(),
      }),
    }),
  ],
})
export class AppModule {}

// 任意のサービスでの使用
@Injectable()
export class AppService {
  constructor(private config: ConfigService) {}

  getPort(): number {
    return this.config.get<number>('PORT', 3000)
  }
}
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { getRepositoryToken } from '@nestjs/typeorm'
import { User } from './entities/user.entity'

describe('UsersService', () => {
  let service: UsersService

  const mockRepository = {
    create: jest.fn(),
    save: jest.fn(),
    findOneBy: jest.fn(),
    findAndCount: jest.fn(),
    delete: jest.fn(),
  }

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository },
      ],
    }).compile()

    service = module.get<UsersService>(UsersService)
  })

  it('should create a user', async () => {
    const dto = { name: 'Alice', email: 'alice@example.com', password: 'secret123' }
    mockRepository.findOneBy.mockResolvedValue(null)
    mockRepository.create.mockReturnValue(dto)
    mockRepository.save.mockResolvedValue({ id: 1, ...dto })

    const result = await service.create(dto)
    expect(result.id).toBe(1)
    expect(mockRepository.save).toHaveBeenCalled()
  })
})
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()
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
    await app.init()
  })

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

  it('/users (POST) creates a user', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com', password: 'secret123' })
      .expect(201)
      .expect((res) => {
        expect(res.body.name).toBe('Alice')
        expect(res.body.id).toBeDefined()
      })
  })

  it('/users (GET) returns paginated list', () => {
    return request(app.getHttpServer())
      .get('/users?page=1&limit=10')
      .expect(200)
      .expect((res) => {
        expect(res.body.items).toBeInstanceOf(Array)
        expect(res.body.total).toBeDefined()
      })
  })
})
  1. リソースジェネレーターを使用するnest g resourceはCRUDスキャフォールド(モジュール、コントローラー、サービス、DTO、エンティティ、テスト)を数秒で作成し、プロジェクトの一貫性を保ちます。

  2. グローバルValidationPipeをwhitelistで有効化whitelist: trueforbidNonWhitelisted: trueを設定して、すべてのリクエストから予期しないフィールドを自動的に除去または拒否します。

  3. DTOにはマップド型を使用PartialTypePickTypeOmitTypeIntersectionTypeで作成DTOから更新/クエリDTOを派生させ、DTOをDRYに保ちます。

  4. コンストラクタインジェクションを優先 — カスタムプロバイダーが必要な場合を除き、手動の@Inject()トークンではなくコンストラクタを通じてNestJSに依存性注入を任せましょう。

  5. コントローラーは薄く保つ — コントローラーは入力のパースと出力の返却のみ行うべきです。テスト可能性と再利用のために、すべてのビジネスロジックをサービスに移動します。

  6. すべての環境アクセスにConfigModuleを使用 — サービスで直接process.envを読み取らないでください。バリデーションスキーマ付きのConfigServiceを使用して、欠落した変数は起動時に即座に失敗するようにします。

  7. ガードとインターセプターのスコープを適切に設定 — 本当にどこにでも適用する場合を除き、グローバルではなくコントローラーまたはルートレベルでデコレーターを使って適用します。

  8. ユニットテストとE2Eテストを書くTest.createTestingModule()でモック依存関係を使用してサービスをユニットテストし、supertestでHTTPエンドポイントをE2Eテストします。

  9. PrismaまたはTypeORMを一貫して使用 — 一つのORMを選んでプロジェクト全体で使用します。Prismaはより良い型安全性を提供し、TypeORMはデコレーターでより柔軟です。

  10. 型ではなく機能で構造化 — ファイルを型(controllers/、services/)ではなくドメイン(users/、auth/、orders/)でグループ化します。これにより関連コードがまとまり、モジュールが自己完結します。

  11. 例外でエラーを処理 — サービスから組み込みHTTP例外(NotFoundExceptionConflictException)をスローします。カスタムエラーフォーマットには例外フィルターを使用します。

  12. async/awaitを一貫して使用 — NestJSはPromiseをネイティブに処理します。サービスメソッドから非同期値を返し、フレームワークにレスポンスのシリアライズを任せましょう。