NestJS
TypeScriptファーストのNode.jsフレームワーク。Angularにインスパイアされたアーキテクチャで、依存性注入とデコレーターを活用したスケーラブルなサーバーサイドアプリケーション開発を実現。
GitHub概要
nestjs/nest
A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀
トピックス
スター履歴
フレームワーク
NestJS
概要
NestJSは、効率的でスケーラブルなNode.jsサーバーサイドアプリケーションを構築するためのTypeScriptファーストのフレームワークです。Angularにインスパイアされたアーキテクチャを採用しています。
詳細
NestJS(ネストジェーエス)は、2017年にKamil Myśliwiecによって開発されたNode.js用のプログレッシブWebフレームワークです。TypeScriptをファーストクラスサポートし、Angularのアーキテクチャパターンを採用したエンタープライズグレードのアプリケーション開発に特化しています。デコレータ(@Injectable、@Controller、@Module等)による宣言的な設計、依存性注入(DI)とIoCコンテナ、モジュラーアーキテクチャ、強力なCLIツール、GraphQL・WebSocket・マイクロサービス対応、豊富なライブラリエコシステムが特徴です。Express.jsやFastifyをベースとして構築され、RESTful API、GraphQL API、マイクロサービス、リアルタイムアプリケーション開発で威力を発揮します。特に大規模なチーム開発やエンタープライズアプリケーションにおいて、保守性と拡張性を重視した設計思想が評価されており、TypeScriptの型安全性とAngularライクな構造化により、長期間の開発プロジェクトに適しています。
メリット・デメリット
メリット
- TypeScript ファースト: 完全な型安全性とIntelliSense対応
- 構造化アーキテクチャ: Angularライクなモジュラー設計
- 依存性注入: テスタブルで保守しやすいコード
- 豊富な機能: GraphQL、WebSocket、マイクロサービス標準サポート
- 強力なCLI: コード生成と開発効率向上
- エンタープライズ対応: 大規模プロジェクトに適した設計
- 豊富なエコシステム: Passport、TypeORM、Mongoose等との統合
- ドキュメント充実: 詳細な公式ドキュメントとサンプル
デメリット
- 学習コストの高さ: TypeScript、デコレータ、DIの理解が必要
- 初期設定の複雑さ: 小規模プロジェクトには過剰な場合も
- パフォーマンス: ミニマルなフレームワークと比較してオーバーヘッド
- デコレータへの依存: 実験的機能への依存度が高い
- バンドルサイズ: ライブラリサイズが比較的大きい
- Angular知識の要求: Angular経験がないと理解に時間がかかる
- 柔軟性の制限: 厳格な構造により自由度が制限される場合
主要リンク
書き方の例
Hello World
// 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);
console.log('NestJS アプリケーションが http://localhost:3000 で起動しました');
}
bootstrap();
// 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();
}
@Get('health')
getHealth() {
return {
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
};
}
}
// app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello, NestJS!';
}
}
// 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 {}
コントローラーとルーティング
// users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
ParseIntPipe,
ValidationPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
async findAll(
@Query('page', ParseIntPipe) page: number = 1,
@Query('limit', ParseIntPipe) limit: number = 10,
): Promise<UserResponseDto[]> {
return this.usersService.findAll(page, limit);
}
@Get(':id')
async findOne(
@Param('id', ParseIntPipe) id: number,
): Promise<UserResponseDto> {
return this.usersService.findOne(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
async create(
@Body(ValidationPipe) createUserDto: CreateUserDto,
): Promise<UserResponseDto> {
return this.usersService.create(createUserDto);
}
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body(ValidationPipe) updateUserDto: UpdateUserDto,
): Promise<UserResponseDto> {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
return this.usersService.remove(id);
}
@Get('search/by-name')
async searchByName(
@Query('name') name: string,
): Promise<UserResponseDto[]> {
return this.usersService.searchByName(name);
}
}
// dto/create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(6)
password: string;
}
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
// dto/user-response.dto.ts
export class UserResponseDto {
id: number;
name: string;
email: string;
createdAt: Date;
updatedAt: Date;
}
サービスと依存性注入
// users.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async findAll(page: number, limit: number): Promise<UserResponseDto[]> {
const [users, total] = await this.userRepository.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
return users.map(user => this.toResponseDto(user));
}
async findOne(id: number): Promise<UserResponseDto> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException('ユーザーが見つかりません');
}
return this.toResponseDto(user);
}
async create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
const existingUser = await this.userRepository.findOne({
where: { email: createUserDto.email },
});
if (existingUser) {
throw new ConflictException('このメールアドレスは既に使用されています');
}
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
const user = this.userRepository.create({
...createUserDto,
password: hashedPassword,
});
const savedUser = await this.userRepository.save(user);
return this.toResponseDto(savedUser);
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
const user = await this.findOne(id); // 存在確認
if (updateUserDto.password) {
updateUserDto.password = await bcrypt.hash(updateUserDto.password, 10);
}
await this.userRepository.update(id, updateUserDto);
const updatedUser = await this.userRepository.findOne({ where: { id } });
return this.toResponseDto(updatedUser);
}
async remove(id: number): Promise<void> {
const user = await this.findOne(id); // 存在確認
await this.userRepository.delete(id);
}
async searchByName(name: string): Promise<UserResponseDto[]> {
const users = await this.userRepository
.createQueryBuilder('user')
.where('user.name LIKE :name', { name: `%${name}%` })
.getMany();
return users.map(user => this.toResponseDto(user));
}
private toResponseDto(user: User): UserResponseDto {
const { password, ...result } = user;
return result;
}
}
// entities/user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
モジュールシステム
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // 他のモジュールで使用するためにエクスポート
})
export class UsersModule {}
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
UsersModule, // UsersServiceを使用するためにインポート
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET || 'your-secret-key',
signOptions: { expiresIn: '7d' },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { User } from './users/entities/user.entity';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'nestjs_app',
entities: [User],
synchronize: process.env.NODE_ENV === 'development',
}),
UsersModule,
AuthModule,
],
})
export class AppModule {}
認証とガード
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<any> {
try {
const user = await this.usersService.findByEmail(email);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
} catch (error) {
return null;
}
}
async login(email: string, password: string) {
const user = await this.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('メールアドレスまたはパスワードが正しくありません');
}
const payload = { email: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
user,
};
}
}
// jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
});
}
async validate(payload: any) {
return { userId: payload.sub, email: payload.email };
}
}
// auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// protected.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';
@Controller('protected')
@UseGuards(JwtAuthGuard)
export class ProtectedController {
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
@Get('admin')
@UseGuards(RolesGuard)
@Roles('admin')
getAdminData() {
return { message: '管理者専用データ' };
}
}
GraphQL統合
// app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
context: ({ req }) => ({ req }),
}),
// 他のモジュール
],
})
export class AppModule {}
// users.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
import { CreateUserInput } from './dto/create-user.input';
import { JwtAuthGuard } from '../auth/auth.guard';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => [User], { name: 'users' })
@UseGuards(JwtAuthGuard)
findAll() {
return this.usersService.findAll(1, 10);
}
@Query(() => User, { name: 'user' })
@UseGuards(JwtAuthGuard)
findOne(@Args('id', { type: () => Int }) id: number) {
return this.usersService.findOne(id);
}
@Mutation(() => User)
createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
return this.usersService.create(createUserInput);
}
}
// entities/user.entity.ts (GraphQL対応)
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { ObjectType, Field, Int } from '@nestjs/graphql';
@Entity()
@ObjectType()
export class User {
@PrimaryGeneratedColumn()
@Field(() => Int)
id: number;
@Column()
@Field()
name: string;
@Column({ unique: true })
@Field()
email: string;
@Column()
password: string; // GraphQLでは公開しない
@Column()
@Field()
createdAt: Date;
}
テストとデバッグ
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
const mockUserRepository = () => ({
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
});
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
describe('UsersService', () => {
let service: UsersService;
let repository: MockRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useFactory: mockUserRepository,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repository = module.get<MockRepository>(getRepositoryToken(User));
});
describe('findOne', () => {
it('ユーザーを正常に取得できること', async () => {
const userId = 1;
const expectedUser = {
id: userId,
name: 'テストユーザー',
email: '[email protected]',
createdAt: new Date(),
updatedAt: new Date(),
};
repository.findOne.mockResolvedValue(expectedUser);
const result = await service.findOne(userId);
expect(repository.findOne).toHaveBeenCalledWith({ where: { id: userId } });
expect(result).toEqual(expectedUser);
});
it('存在しないユーザーIDの場合は例外を投げること', async () => {
const userId = 999;
repository.findOne.mockResolvedValue(null);
await expect(service.findOne(userId)).rejects.toThrow('ユーザーが見つかりません');
});
});
describe('create', () => {
it('新しいユーザーを作成できること', async () => {
const createUserDto = {
name: '新規ユーザー',
email: '[email protected]',
password: 'password123',
};
const savedUser = {
id: 1,
...createUserDto,
password: 'hashed_password',
createdAt: new Date(),
updatedAt: new Date(),
};
repository.findOne.mockResolvedValue(null); // 既存ユーザーなし
repository.create.mockReturnValue(savedUser);
repository.save.mockResolvedValue(savedUser);
const result = await service.create(createUserDto);
expect(repository.save).toHaveBeenCalled();
expect(result).toEqual(expect.objectContaining({
id: 1,
name: createUserDto.name,
email: createUserDto.email,
}));
});
});
});
// users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
const mockUsersService = () => ({
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
});
describe('UsersController', () => {
let controller: UsersController;
let service: jest.Mocked<UsersService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useFactory: mockUsersService,
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get(UsersService);
});
describe('findAll', () => {
it('全ユーザーのリストを返すこと', async () => {
const expectedUsers = [
{ id: 1, name: 'ユーザー1', email: '[email protected]' },
{ id: 2, name: 'ユーザー2', email: '[email protected]' },
];
service.findAll.mockResolvedValue(expectedUsers);
const result = await controller.findAll(1, 10);
expect(service.findAll).toHaveBeenCalledWith(1, 10);
expect(result).toEqual(expectedUsers);
});
});
});