※ 本記事に掲載しているコード例は実際のプロダクトをベースにしていますが、プロジェクト名・サービス名・変数名など、特定のプロダクトを識別できる情報は変更・汎用化して記載しています。
(0から全部自分で書いてなく、AI生成したのを修正しプロジェクトの辞書の様に使用する目的)
NestJSはTypeScriptで構築されたサーバーサイドフレームワークで、Angularに影響を受けたモジュール指向のアーキテクチャが特徴です。本記事ではNestJSの基本概念を解説しつつ、実際のSaaSプロダクト(GraphQL API / マルチテナント構成)でどのように活用しているかを紹介します。
目次
- Module(モジュール)
- Controller / Resolver
- Service / UseCase
- DI(依存性注入)
- Guard(認証・認可)
- Entity定義の多層構造
- Prisma連携(Repository + Mapper)
- 認証コンテキストの流れ
- まとめ:NestJS標準 vs 実プロジェクト
1. Module(モジュール)
NestJSの基本
NestJSのアプリケーションは Module を単位として構成されます。@Module() デコレータで providers(提供するサービス)、imports(依存するモジュール)、exports(外部に公開するサービス)を定義し、DIコンテナを構成します。
@Module({
providers: [CatService],
imports: [DatabaseModule],
exports: [CatService],
})
export class CatModule {}
実プロジェクトでの活用
本プロジェクトでは、レイヤードアーキテクチャに沿って3層のモジュールに分離しています。
app.module.ts
├─ ResolverModule群(interface/di/) ← プレゼンテーション層
├─ UseCaseModule群(app/di/) ← アプリケーション層
└─ RepositoryModule群(infra/prisma/di/) ← インフラ層
ディレクトリ構成は以下のようになっています。
packages/api/src/
├── app/ # Application層
│ ├── di/ # UseCaseモジュール定義
│ └── useCases/ # UseCase実装
├── domain/ # Domain層
│ ├── entities/ # ドメインエンティティ・型定義
│ ├── restrictFilters/ # 権限フィルター
│ └── common/ # 共通型・ユーティリティ
├── interface/ # Interface層
│ ├── di/ # Resolverモジュール定義
│ ├── resolvers/ # GraphQL Resolver
│ ├── schema/ # リクエスト/レスポンス型
│ └── auth/ # 認証Guard
├── infra/ # Infrastructure層
│ ├── prisma/ # Repository・Mapper
│ └── ... # 外部サービス連携
└── entities/ # GraphQL ObjectType定義
ResolverModuleは、UseCaseModuleを imports して依存を解決します。
@Module({
providers: [CommentResolver],
imports: [
CommentUseCaseModule,
UserUseCaseModule,
],
})
export class CommentResolverModule {}
UseCaseModuleは、RepositoryModuleや他のServiceModuleを imports します。
@Module({
providers: [CommentUseCase],
exports: [CommentUseCase],
imports: [
PrismaCommentRepositoryModule,
PrismaRoomRepositoryModule,
NotificationServiceModule,
],
})
export class CommentUseCaseModule {}
ポイント: NestJSのModule機構を活用して、クリーンアーキテクチャ的な依存の方向(interface → app → domain ← infra)を実現しています。
2. Controller / Resolver
NestJSの基本
REST APIの場合は @Controller() に @Get() / @Post() などのデコレータを使います。
@Controller('cats')
export class CatController {
@Get()
findAll() { return 'All cats'; }
@Post()
create(@Body() dto: CreateCatDto) { ... }
}
GraphQLの場合は @Resolver() に @Query() / @Mutation() / @ResolveField() を使います。
実プロジェクトでの活用
本プロジェクトではGraphQLを採用し、Code-Firstアプローチでスキーマを自動生成しています。
// app.module.ts
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // TypeScriptから自動生成
fieldResolverEnhancers: ['guards'],
}),
Resolverの実装例:
@Resolver(() => GqlComment)
export class CommentResolver {
constructor(private readonly commentUseCase: CommentUseCase) {}
// Query: データ取得
@UseGuards(AuthGuard)
@Query(() => [GqlComment])
async commentList(
@Args('input') input: GetCommentListInput,
@Context() context: OperationContext,
) {
return this.commentUseCase.getCommentList(input, context);
}
// Mutation: データ変更
@UseGuards(AuthGuard)
@Mutation(() => GqlComment)
async createComment(
@Args('input') input: CreateCommentInput,
@Context() context: OperationContext,
) {
return this.commentUseCase.createComment(input, context);
}
// ResolveField: ネストされたフィールドの解決
@ResolveField('author', () => GqlUser)
async getAuthor(@Parent() comment: Comment) {
return this.userUseCase.findOne({ id: comment.userId });
}
}
主なデコレータ
| デコレータ | 用途 |
|---|---|
@Resolver(() => Type) |
クラスをResolver宣言 |
@Query(() => Type) |
GraphQL Query定義 |
@Mutation(() => Type) |
GraphQL Mutation定義 |
@ResolveField('name', () => Type) |
ネストフィールドの解決 |
@Args('input') |
入力引数のバインド |
@Parent() |
親オブジェクトへのアクセス |
@Context() |
GraphQL Context(認証情報等)の取得 |
InputType(リクエスト型)
GraphQLの入力型もデコレータで定義します。
@InputType()
export class CreateCommentInput {
@Field(() => GraphQLJSON)
content: Prisma.JsonObject;
@Field()
roomId: string;
@Field(() => [Int], { nullable: true })
imageIds?: number[];
}
これにより、以下のGraphQLスキーマが自動生成されます。
input CreateCommentInput {
content: JSON!
roomId: ID!
imageIds: [Int!]
}
3. Service / UseCase
NestJSの基本
@Injectable() デコレータを付けたクラスをServiceとして使い、ビジネスロジックを集約します。
@Injectable()
export class CatService {
constructor(private readonly catRepository: CatRepository) {}
findAll() {
return this.catRepository.findAll();
}
}
実プロジェクトでの活用
本プロジェクトでは「Service」ではなく 「UseCase」 と命名し、ユースケース単位でクラスを分割しています。
@Injectable()
export class CommentUseCase {
constructor(
private readonly commentRepository: PrismaCommentRepository,
private readonly roomRepository: PrismaRoomRepository,
private readonly notificationService: NotificationService,
) {}
async createComment(input, context: OperationContext) {
// 1. ドメイン検証
const room = CoreAssert.isNotEmpty(
await this.roomRepository.findOneById({ id: input.roomId }, context),
);
// 2. ドメインエンティティ作成
const comment = createCommentSeed({
...input,
userId: context.executor.userId,
});
// 3. DB保存
const created = await this.commentRepository.create(comment);
// 4. 通知・外部連携
await this.notificationService.send({ ... });
return created;
}
}
ポイント: UseCase内でドメイン検証 → DB保存 → 副作用(通知・外部連携)の順で処理を組み立てています。Repositoryや外部サービスは全てコンストラクタインジェクションで注入されるため、テスタビリティが高いです。
4. DI(依存性注入)
NestJSの基本
NestJSのDIは、TypeScriptの型情報からプロバイダーを自動解決します。@Injectable() クラスをModuleの providers に登録し、コンストラクタの型で自動注入されます。
実プロジェクトでの活用
層をまたぐ依存は imports / exports で制御しています。
Resolver → constructor(useCase: CommentUseCase) // 型で自動解決
UseCase → constructor(repo: PrismaCommentRepository) // 型で自動解決
Module定義側:
// UseCaseModule: CommentUseCaseを外部に公開
@Module({
providers: [CommentUseCase],
exports: [CommentUseCase], // 他のモジュールから利用可能に
imports: [PrismaCommentRepositoryModule],
})
export class CommentUseCaseModule {}
// ResolverModule: UseCaseModuleをimportして利用
@Module({
providers: [CommentResolver],
imports: [CommentUseCaseModule], // CommentUseCaseが利用可能に
})
export class CommentResolverModule {}
循環依存が発生する場合は forwardRef() で回避します。
@Module({
imports: [forwardRef(() => RelatedModule)],
})
export class SomeModule {}
5. Guard(認証・認可)
NestJSの基本
CanActivate インターフェースを実装したGuardクラスを作成し、@UseGuards() デコレータで適用します。
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return validateToken(request.headers.authorization);
}
}
実プロジェクトでの活用
本プロジェクトはマルチユーザー型SaaSのため、ユーザータイプ(管理者 / ゲスト)に応じて異なる認証Guardを振り分けるパターンを採用しています。
@Injectable()
export class MultiUserAuthGuard implements CanActivate {
constructor(
private readonly adminAuthGuard: AdminAuthGuard,
private readonly guestAuthGuard: GuestAuthGuard,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
const userType = request.headers['x-user-type'];
if (userType === 'Admin') {
// JWT検証(外部認証サービス)
return this.adminAuthGuard.canActivate(context);
} else if (userType === 'Guest') {
// セッショントークン検証
return this.guestAuthGuard.canActivate(context);
}
throw new UnauthorizedException();
}
}
さらに @ResolveField レベルでの細かい権限制御も実装しています。
@ResolveField('author', () => GqlUser)
@UseGuards(UserTypesGuard)
@RequiredUserTypes('ADMIN') // カスタムデコレータでメタデータ指定
async getAuthor(@Parent() comment: Comment) {
return this.userUseCase.findOne({ id: comment.userId });
}
UserTypesGuard は Reflector を使ってメタデータを読み取り、権限チェックを行います。
@Injectable()
export class UserTypesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredTypes = this.reflector.get<string[]>(
'requiredUserTypes',
context.getHandler(),
);
if (!requiredTypes) return true;
const gqlContext = GqlExecutionContext.create(context);
const user = gqlContext.getContext().req.user;
return requiredTypes.includes(user.executor.userType);
}
}
6. Entity定義の多層構造
NestJSの基本
一般的なNestJSアプリケーションでは、1つのEntityクラスがDB定義とAPI定義を兼ねることが多いです。
実プロジェクトでの活用
本プロジェクトでは3層のEntity定義を使い分けています。
| 層 | ファイル例 | 用途 |
|---|---|---|
| GraphQL Entity | entities/Comment.entity.ts |
@ObjectType() でAPI出力型を定義 |
| Domain Entity | domain/entities/Comment/index.ts |
Brand型による型安全なビジネスモデル |
| Prisma Model | prisma/schema.prisma |
DBスキーマ定義 |
GraphQL Entity(API層)
@ObjectType()
export class Comment {
@Field(() => Int)
id: number;
@Field(() => GraphQLJSON)
content: Prisma.JsonObject;
@Field()
createdAt: Date;
@Field(() => [CommentImage])
commentImages: CommentImage[]; // ネストフィールド
}
Domain Entity(ドメイン層)
Brand型を使って、同じ number 型でもIDの種類を区別します。
// Brand型の定義
type Brand<T, U> = T & { __brand: U };
// IDを型レベルで区別
export type CommentId = Brand<number, 'CommentId'>;
export type UserId = Brand<number, 'UserId'>;
// 型エラー: CommentId と UserId は互換性がない
const commentId: CommentId = 1 as CommentId;
const userId: UserId = commentId; // コンパイルエラー!
ドメインエンティティの定義:
export type Comment = {
id: CommentId;
content: Prisma.JsonObject;
roomId: RoomId;
userId: UserId;
tenantId: TenantId;
createdAt: Date;
updatedAt: Date;
};
// ファクトリ関数
export const createCommentSeed = (payload: CommentSeed): CommentSeed => {
return { ...payload };
};
Prisma Model(DB層)
model Comment {
id Int @id @default(autoincrement())
content Json?
roomId String
Room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
ポイント: 3層に分けることで、API仕様の変更がドメインロジックに影響しない、DBスキーマの変更がAPI層に漏れないという利点があります。
7. Prisma連携(Repository + Mapper)
NestJSの基本
NestJSではTypeORMやPrismaを直接サービスから利用するのが一般的です。
実プロジェクトでの活用
本プロジェクトではRepository + Mapperパターンを採用し、Prismaの型をドメイン型に変換しています。
Repository
@Injectable()
export class PrismaCommentRepository {
async create(input: CommentSeed, db = client): Promise<Comment> {
const prismaComment = await db.comment.create({
data: {
contentJson: input.content,
roomId: input.roomId,
},
include: { Room: true },
});
// Prisma型 → Domain型への変換
return PrismaCommentMapper.toDomainComment(prismaComment);
}
async findList(input, context, db = client): Promise<Comment[]> {
const results = await db.comment.findMany({
where: {
roomId: input.roomId,
Room: { tenantId: context.tenantId }, // テナント隔離
},
orderBy: { createdAt: 'asc' },
});
return results.map(PrismaCommentMapper.toDomainComment);
}
}
Mapper
export class PrismaCommentMapper {
static toDomainComment(prismaComment): Comment {
return {
id: prismaComment.id as CommentId,
content: prismaComment.contentJson as Prisma.JsonObject,
roomId: prismaComment.roomId as RoomId,
userId: prismaComment.userId as UserId,
tenantId: prismaComment.Room.tenantId as TenantId,
createdAt: prismaComment.createdAt,
updatedAt: prismaComment.updatedAt,
};
}
}
トランザクション
複数テーブルにまたがる操作は prisma.$transaction() で原子性を保証します。
const result = await prisma.$transaction(async (db) => {
const room = await this.roomRepository.create(roomData, context, db);
await this.memberRepository.create(memberData, db);
return room;
});
ポイント: Repositoryメソッドはオプショナルな db 引数を受け取り、トランザクション内でもスタンドアロンでも動作するよう設計されています。
8. 認証コンテキストの流れ
マルチテナントSaaSにおける認証情報の伝搬フローです。
1. GraphQLリクエスト
├─ Header: Authorization: "Bearer <JWT>"
└─ Header: x-user-type: "Admin" or "Guest"
2. Guard(認証処理)
├─ Admin → JWT検証 → OperationContext生成
└─ Guest → セッション検証 → OperationContext生成
└─ request.user = OperationContext にセット
3. Resolver
└─ @Context() context: OperationContext で取得
4. UseCase / Repository
└─ contextを利用した権限制御・テナント隔離
OperationContext はZodスキーマで型安全に定義されています。
export const operationContextSchema = z.object({
tenantId: tenantIdSchema,
executor: z.union([
z.object({
userType: z.literal('ADMIN'),
userId: userIdSchema,
role: roleSchema,
}),
z.object({
userType: z.literal('GUEST'),
userId: userIdSchema,
}),
]),
});
export type OperationContext = z.infer<typeof operationContextSchema>;
カスタムデコレータ @Context() でResolverに注入します。
export const Context = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user as OperationContext;
},
);
権限フィルターでDB操作時のテナント隔離を行います。
export const commentRestrictFilter = (
context: OperationContext,
): Prisma.CommentWhereInput[] => {
return [{ Room: { tenantId: context.tenantId } }];
};
// 使用例
const comments = await prisma.comment.findMany({
where: { AND: commentRestrictFilter(context) },
});
9. まとめ:NestJS標準 vs 実プロジェクト
| 観点 | NestJS標準 | 実プロジェクト |
|---|---|---|
| API方式 | REST (@Controller) |
GraphQL Code-First (@Resolver) |
| ビジネスロジック | Service | UseCase(ユースケース単位で分割) |
| データアクセス | ORM直接利用 | Repository + Mapper パターン |
| 認証 | 単一Guard | MultiUser振り分けGuard |
| 型安全 | TypeScript標準 | Brand型 + Zod バリデーション |
| テナント隔離 | なし | OperationContext + RestrictFilter |
| Entity | 単一定義 | 3層(GraphQL / Domain / Prisma) |
| Module構成 | 機能単位 | レイヤー単位(Resolver / UseCase / Repository) |
おわりに
NestJSはそのままでも十分に強力なフレームワークですが、プロジェクトの規模や要件に応じてアーキテクチャをカスタマイズできる柔軟性も持っています。
本プロジェクトでは、NestJSのDI機構を活かしつつ、以下のような工夫を加えています。
- レイヤードアーキテクチャ: Module単位でレイヤーを分離し、依存の方向を制御
- Code-First GraphQL: TypeScriptのデコレータからスキーマを自動生成
- Brand型: IDの取り違えをコンパイル時に検出
- RestrictFilter: マルチテナントのデータ隔離をパターン化
NestJSの基本概念(Module / DI / Guard / Decorator)を理解しておけば、プロジェクトの要件に合わせた拡張も容易です。ぜひ参考にしてみてください。
Top comments (0)