こんにちは、naoです。
今回はNestJS、Next.js、GraphQLでTodoアプリを作っていこうと思います。
2回に分けて行うので今回はbackendの実装となります。
frontendは以下から
こちらもCHECK
-
-
NestJS、Next.js、GraphQLでTODOアプリ[フロントエンド]
続きを見る
今回のソースコードは以下となります。
https://github.com/109naoki/nest-graph-backend
目次
環境構築
作業用のディレクトリを作ってその中でまずはbackendの構築をしていきましょう。
commandnest new backend //パッケージマネージャを聞かれるのでnpmを選択
プロジェクトディレクトリに移動し、srcディレクトリに移動をして
main.ts、app.module.ts以外のファイルは削除をして、app.module.tsの中身を以下のようにする
app.module.ts
import { Module } from '@nestjs/common';
@Module({
imports: [],
})
export class AppModule {}
GraphQLのセットアップ
backendディレクトリに移動をして以下のライブラリをインストールします。
command
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql
インストールが完了したら
main.tsとapp.module.tsを以下のように書き換えてください
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
}
bootstrap();
app.module.ts
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: true,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
],
})
export class AppModule {}
.forRoot<ApolloDriverConfig>()
: これは、GraphQLモジュールを構成するためのメソッドです。型ApolloDriverConfig
で指定された設定オプションを受け取ります。driver: ApolloDriver
: 使用するドライバとしてApolloを指定しています。playground: true
: GraphQL Playground(APIをテストするためのUI)を有効にしています。autoSchemaFile
: スキーマファイルの位置を指定しています。この設定では、プロジェクトのルートからsrc/schema.gql
を使用することを示しています。
Taskモデルの作成
ターミナルで以下のコマンドを入力してmoduleを作成します。
command
nest g module task
これによりtaskディレクトリにtask.module.tsが作成され
app.module.tsでもtask.module.tsが使用できるようになっています。
app.module.ts
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { TaskModule } from './task/task.module';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: true,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
TaskModule,
],
})
export class AppModule {}
次にGraphQLのスキーマを定義するモデルを作成します。
task/models配下にtask.model.tsを作成してください。
task/models/task.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Task {
@Field(() => Int)
id: number;
@Field()
name: string;
@Field()
dueDate: string;
@Field()
status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED';
@Field({ nullable: true })
description: string;
}
- @ObjectType()デコレータ:
このデコレータは、 ()
Task
クラスをGraphQLのオブジェクトタイプとして定義するためのものです。GraphQLでは、オブジェクトタイプはクエリの主要な構成要素として機能します。 - プロパティと@Fieldデコレータ:
@Field(() => Int) id: number;
: これはid
という名前のフィールドを定義しています。このフィールドはInt
(整数)タイプのものとして表現されます。@Field() name: string;
:name
という名前のフィールドを定義しています。デフォルトで文字列タイプとして扱われます。@Field() dueDate: string;
:dueDate
という名前のフィールドも文字列タイプとして定義されています。@Field() status: 'NOT_STARTED' | 'IN_PROGRESS' | 'COMPLETED';
:status
という名前のフィールドを定義しています。このフィールドは3つの文字列リテラル型のいずれかの値を取ることができます。@Field({ nullable: true }) description: string;
:description
という名前のフィールドを定義しています。nullable: true
オプションが指定されているので、このフィールドはnull値を取ることが許容されます。
リゾルバー、serviceの実装
ターミナルで以下のコマンドを入力してください。
command
nest g resolver task --no-spec
nest g service task --no-spec
これによりtask.resolverとserviceが作成されtask.moduleのprovidersに自動でインポートされます。
task.module.ts
import { Module } from '@nestjs/common';
import { TaskResolver } from './task.resolver';
import { TaskService } from './task.service';
@Module({
providers: [TaskResolver, TaskService],
})
export class TaskModule {}
次にサービスの中身を記述してください。
task.service.ts
import { Injectable } from '@nestjs/common';
import { Task } from './models/task.model';
@Injectable()
export class TaskService {
tasks: Task[] = [];
getTasks(): Task[] {
const task1 = new Task();
task1.id = 1;
task1.name = 'task1';
task1.dueDate = '2023-01-01';
task1.status = 'NOT_STARTED';
this.tasks.push(task1);
return this.tasks;
}
}
@Injectable()デコレータ:
@Injectable()は、このクラスがNestJSの依存関係注入(DI)システムによってインスタンス化・管理されることを示します。
これにより、他のコントローラやサービスでこのTaskServiceを注入して利用することができます。
TaskServiceクラス:
export class TaskService {
tasks: Task[] = [];
...
}
この部分では、TaskServiceクラスを定義しています。このクラスには、タスクのリストを保持するtasksプロパティがあり、初期状態では空の配列になっています。
getTasksメソッド:
getTasks(): Task[] {
const task1 = new Task();
task1.id = 1;
task1.name = 'task1';
task1.dueDate = '2023-01-01';
task1.status = 'NOT_STARTED';
this.tasks.push(task1);
return this.tasks;
}
このメソッドは、タスクのリストを取得するためのものです。現在の実装では、新しいタスクを作成し、それをtasks配列に追加し、その配列を返しています。
新しいTaskインスタンスを作成して、そのプロパティに固定の値をセットしています。
セットアップしたタスクをtasks配列に追加します。
最終的にtasks配列を返しています。
現在の実装では毎回同じタスクを追加するだけですが、実際の用途ではデータベースや外部APIからタスクデータを取得するようなロジックが組み込まれることが考えられます。
次にresolverを作成します。
html
import { Resolver, Query } from '@nestjs/graphql';
import { TaskService } from './task.service';
import { Task } from './models/task.model';
@Resolver()
export class TaskResolver {
constructor(private readonly taskService: TaskService) {}
@Query(() => [Task], { nullable: 'items' })
getTasks(): Task[] {
return this.taskService.getTasks();
}
}
@Resolver()デコレータ:
@Resolverデコレータは、このクラスがGraphQLリゾルバとして機能することを示します。
TaskResolverクラス:
export class TaskResolver {
constructor(private readonly taskService: TaskService) {}
...
}
TaskResolverクラスが定義されており、この中にGraphQLのクエリに対するリゾルバ関数を含めることができます。また、クラスのコンストラクタにTaskServiceを注入して、このサービスのメソッドをリゾルバ内で使用できるようにしています。
@QueryデコレータとgetTasksメソッド:
@Query(() => [Task], { nullable: 'items' })
getTasks(): Task[] {
return this.taskService.getTasks();
}
@Queryデコレータは、このメソッドがGraphQLのクエリとして利用されることを示します。getTasksという名前のクエリがGraphQLエンドポイントにリクエストされると、このメソッドが実行されます。
() => [Task] は、このクエリの戻り値がTaskの配列であることを示しています。
{ nullable: 'items' } は、配列内の各アイテムがnullである可能性があることを示しています。
メソッド内部では、注入されたtaskServiceのgetTasksメソッドを呼び出し、その結果を返しています。
このTaskResolverクラスを使用することで、フロントエンドや他のクライアントからのGraphQLクエリでgetTasksをリクエストすると、対応するタスクのリストが返されます。
GraphQLスキーマの確認
backendディレクトリでサーバを立ち上げます。
command
npm run start:dev
するとsrcディレクトリにschema.gqlファイルが作成されています。
schema.gql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type Task {
id: Int!
name: String!
dueDate: String!
status: String!
description: String
}
type Query {
getTasks: [Task]!
}
type
キーワードを使用してTask
という新しい型を定義しています。この型は以下のフィールドを持ちます:
id
: 整数型(Int
)のタスクIDで、!
はこのフィールドが必須であることを示す。name
: 文字列型(String
)のタスク名で、必須フィールド。dueDate
: 文字列型(String
)の期限日で、必須フィールド。status
: 文字列型(String
)のタスクの状態で、必須フィールド。description
: 文字列型(String
)のタスクの説明。このフィールドはオプショナルで、タスクに説明がない場合はnullを返すことができます。
type Query
は、GraphQLのエントリポイントであり、可能なクエリを定義する場所です。このスキーマ定義では、getTasks
という名前のクエリがあり、その結果はTask
の配列を返すことを示しています。!
は、このクエリがnullを返すことはない(つまり、常にTask
のリストを返す)ことを示していますが、配列内の各アイテムがnullになる可能性は残しています。
playgroundから実行
サーバを立ち上げた状態で以下のURLのアクセスをしてください
http://localhost:3000/graphql
タスク作成機能の実装
次はタスクを作成機能を作ります。
task.service.tsに戻り以下のコードを追加します。
task.service.ts
import { Injectable } from '@nestjs/common';
import { Task } from './models/task.model';
@Injectable()
export class TaskService {
tasks: Task[] = [];
getTasks(): Task[] {
return this.tasks;
}
createTask(name: string, dueDate: string, description?: string): Task {
const newTask = new Task();
newTask.id = this.tasks.length + 1;
newTask.name = name;
newTask.dueDate = dueDate;
newTask.status = 'NOT_STARTED';
newTask.description = description;
this.tasks.push(newTask);
return newTask;
}
}
次にresolverを作成していきます。
task.resolver.ts
@Mutation(() => Task)
createTask(
@Args('name') name: string,
@Args('dueDate') dueDate: string,
@Args('description', { nullable: true }) description: string,
): Task {
return this.taskService.createTask(name, dueDate, description);
}
@Mutation() デコレータ:
@Mutation(() => Task)
@Mutationデコレータは、この関数がGraphQLのミューテーションであることを示しています。このミューテーションはTask型のオブジェクトを返します。
関数定義:
createTask(
@Args('name') name: string,
@Args('dueDate') dueDate: string,
@Args('description', { nullable: true }) description: string,
): Task {
createTaskという名前の関数を定義しています。この関数は、3つの引数を受け取ります:
name: タスクの名前
dueDate: タスクの期限日
description: タスクの説明(オプショナル)
これらの引数は、GraphQLのミューテーションを呼び出す際に指定されます。
@Args() デコレータ:
各引数の前に配置されている@Args()デコレータは、GraphQLのミューテーションから引数を取得するためのものです。引数名として指定された文字列は、GraphQLのミューテーションを呼び出す際にクライアントが指定するフィールド名と一致します。
@Args('name')は、クライアントがnameフィールドとして指定する文字列をname変数にバインドします。
@Args('description', { nullable: true })のように、nullableオプションを指定することで、このフィールドがオプショナルであることを示しています。
保存をするとschema.gqlにもMutationが追加されているのがわかります。
html
type Mutation {
createTask(name: String!, dueDate: String!, description: String): Task!
}
実際にplaygroundからもタスクの追加ができます。
DTOを使用した入力値バリデーション
Nest.jsにおけるDTO(Data Transfer Object)は、クライアントとサーバ間のデータのやり取りを表現するオブジェクトです。DTOは、APIのエンドポイントに対する入力データの形状や出力データの形状を型付けする役割を果たします。これにより、APIが受け取るデータや返すデータの構造が明確になり、エンドポイントの動作がより予測可能になります。
Nest.jsでは、DTOは通常、クラスとして定義され、デコレータを使用してフィールドにメタデータを追加します。これらのデコレータは、データのバリデーションや変換、Swaggerドキュメント生成などの機能に使われます。
今のままだと空でも登録することができるので制限できるようにしましょう。
taskディレクトリにdtoフォルダを作成し、createTask.input.tsを作成します。
createTask.input.ts
import { Field, InputType } from '@nestjs/graphql';
@InputType()
export class CreateTaskInput {
@Field()
name: string;
@Field()
dueDate: string;
@Field({ nullable: true })
description?: string;
}
次に以下のライブラリをインストールします。
command
npm i class-validator class-transformer
class-validator:
このライブラリを使用すると、クラスのプロパティにデコレータを付けることで、それに関連するバリデーションルールを簡単に定義できます。
例えば、@IsInt(), @IsString(), @Length(10, 20) などのデコレータを使用して、フィールドが整数であること、文字列であること、または特定の長さの範囲を持っていることを指定できます。
バリデーションは、validate 関数を使用して行うことができ、これによりオブジェクトのプロパティが定義したルールに従っているかどうかをチェックできます。
class-transformer:
このライブラリは、プレーンなJavaScriptオブジェクトをクラスのインスタンスに変換したり、その逆の変換をしたりするのに役立ちます。
これにより、例えば、APIのレスポンスを特定のクラスのインスタンスに変換したり、APIのリクエストボディをバリデーションと共に特定のクラスのインスタンスに変換することが容易になります。
@Transform デコレータを使用して、特定のプロパティにカスタムの変換ロジックを適用することも可能です。
ドキュメント:https://github.com/typestack/class-validator#validation-decorators
ライブラリをインストール後createTask.input.tsを再度編集します。
createTask.input.ts
import { Field, InputType } from '@nestjs/graphql';
import { IsDateString, IsNotEmpty } from 'class-validator';
@InputType()
export class CreateTaskInput {
@Field()
@IsNotEmpty()
name: string;
@Field()
@IsDateString()
dueDate: string;
@Field({ nullable: true })
description?: string;
}
次にこのバリデーションを機能させるためにmain.tsを以下のようにします。
main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.enableCors();
await app.listen(3000);
}
bootstrap();
バリデーションが機能するようになったのでtask.resolver.tsのcreateTaskを編集します。
task.resolver.ts
@Mutation(() => Task)
createTask(@Args('createTaskInput') creteTaskInput: CreateTaskInput): Task {
return this.taskService.createTask(creteTaskInput);
}
task.service.tsのcreateTaskメソッドも編集します。
taskservice.ts
createTask(createTaskInput: CreateTaskInput): Task {
const { name, dueDate, description } = createTaskInput;
const newTask = new Task();
newTask.id = this.tasks.length + 1;
newTask.name = name;
newTask.dueDate = dueDate;
newTask.status = 'NOT_STARTED';
newTask.description = description;
this.tasks.push(newTask);
return newTask;
}
schema.gqlにも反映されています。
schema.gql
input CreateTaskInput {
name: String!
dueDate: String!
description: String
}
playgroundでもエラーが返されるようになりました。
バックエンド実装(データベース)
今まではデータを挿入するといったことはしてなかったので
データベース環境を構築してデータを挿入できるようにしましょう。
backendフォルダの直下にdocker-compose.ymlを作成して中身を以下のようにしてください
docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: postgres
ports:
- 5432:5432
volumes:
- ./docker/postgres/init.d:/docker-entrypoint-initdb.d
- ./docker/postgres/pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: my_user
POSTGRES_PASSWORD: my_pass
POSTGRES_INITDB_ARGS: '--encoding=UTF-8'
POSTGRES_DB: my_db
hostname: postgres
restart: always
user: root
以下のコマンドでdockerを起動させます。
docker-compose up -d
prismaのセットアップ
backendディレクトリでprismaをインストールし、初期化を行います。
command
npm install prisma --save-dev
npx prisma init
.envのDATABASE_URLを書き換えます
DATABASE_URL="postgresql://my_user:my_pass@localhost:5434/mydb?schema=public"
schema.prismaにTaskモデルを定義します。
schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Task{
id Int @id @default(autoincrement())
name String @db.VarChar(255)
dueDate String @db.VarChar(10)
status Status @default(NOT_STARTED)
description String?
createdAt DateTime @default(now()) @db.Timestamp(0)
updatedAt DateTime @updatedAt @db.Timestamp(0)
}
enum Status{
NOT_STARTED
IN_PROGRESS
COMPLETED
}
migrationをする
npx prisma migrate dev --name init
テーブルの確認ができる
npx prisma studio
prismaクライアントのインストールをします。
command
npm i @prisma/client
prismaサービスとmoduleの作成
command
nest g module prisma
nest g service prisma --no-spec
prisma.service.tsを以下のように書き換えてください
prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}
その後にprisma.module.tsのexportsに追加をします。
prisma.module.ts
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
タスク一覧と作成の修正
prismaを使いデータを保存できるようになったので先程のコードを修正しましょう。
まずはtask.model.tsを開きstatusをprismaで定義したStatusにかえます。
html
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Status } from '@prisma/client';
@ObjectType()
export class Task {
@Field(() => Int)
id: number;
@Field()
name: string;
@Field()
dueDate: string;
@Field()
status: Status;
@Field({ nullable: true })
description: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
}
そしてtask.module.tsでPrismaModuleをインポートします。
task.module.ts
@Module({
imports: [PrismaModule],
providers: [TaskResolver, TaskService],
})
次にtask.service.tsを編集します。
task.service.ts
import { Injectable } from '@nestjs/common';
import { CreateTaskInput } from './dto/createTask.input';
import { PrismaService } from 'src/prisma/prisma.service';
import { Task } from '@prisma/client';
@Injectable()
export class TaskService {
constructor(private readonly prismaService: PrismaService) {}
async getTasks(): Promise<Task[]> {
return await this.prismaService.task.findMany();
}
async createTask(createTaskInput: CreateTaskInput): Promise<Task> {
const { name, dueDate, description } = createTaskInput;
return await this.prismaService.task.create({
data: {
name,
dueDate,
description,
},
});
}
}
コンストラクタ:
prismaServiceという名前のPrismaServiceインスタンスをprivate readonlyフィールドとして注入しています。これにより、クラス内のメソッドでPrismaを使用してデータベース操作を行うことができます。
getTasksメソッド:
このメソッドは、すべてのタスクをデータベースから取得するためのものです。
this.prismaService.task.findMany()を使用して、タスクの全レコードを取得しています。
createTaskメソッド:
タスクをデータベースに作成するためのメソッドです。
メソッドの引数createTaskInputは、タスクを作成するための必要な情報(name, dueDate, description)を含むオブジェクトです。
this.prismaService.task.createメソッドを使用して、新しいタスクをデータベースに追加しています。このとき、dataオプションに入力データを指定しています。
次にtask.resolverの修正を行います。
task.resolver.ts
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { TaskService } from './task.service';
import { Task as TaskModel } from './models/task.model';
import { CreateTaskInput } from './dto/createTask.input';
import { Task } from '@prisma/client';
@Resolver()
export class TaskResolver {
constructor(private readonly taskService: TaskService) {}
@Query(() => [TaskModel], { nullable: 'items' })
async getTasks(): Promise<Task[]> {
return await this.taskService.getTasks();
}
@Mutation(() => TaskModel)
async createTask(
@Args('createTaskInput') creteTaskInput: CreateTaskInput,
): Promise<Task> {
return await this.taskService.createTask(creteTaskInput);
}
}
コンストラクタ:
TaskServiceインスタンスをprivate readonlyフィールドとして注入しています。これにより、クラス内のメソッドでTaskServiceの提供するデータベース操作を呼び出すことができます。
getTasksメソッド:
@Queryデコレータは、このメソッドがGraphQLのクエリリゾルバとして動作することを示しています。
クエリはTaskModelの配列を返し、アイテムがnullの場合でも問題ありません(nullable: 'items')。
このメソッドは、TaskServiceを使用してデータベースからタスクのリストを取得しています。
createTaskメソッド:
@Mutationデコレータは、このメソッドがGraphQLのミューテーションリゾルバとして動作することを示しています。
メソッドの引数は、@Argsデコレータを使用してクライアントから提供されたcreateTaskInputを取得しています。
このメソッドは、TaskServiceを使用して新しいタスクをデータベースに追加しています。
これでサーバを立ち上げてplaygroundで動作確認をしてprismaにデータが挿入できれば完了です。
データの更新と削除
次に更新と削除を行います。
まずdtoディレクトリにupdateTask.input.tsを作成します。
updateTask.input.ts
import { Field, InputType, Int } from '@nestjs/graphql';
import { Status } from '@prisma/client';
import { IsDateString, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
@InputType()
export class UpdateTaskInput {
@Field(() => Int)
id: number;
@Field({ nullable: true })
@IsNotEmpty()
@IsOptional()
name?: string;
@Field({ nullable: true })
@IsDateString()
@IsOptional()
dueDate?: string;
@Field({ nullable: true })
@IsEnum(Status)
@IsOptional()
status?: Status;
@Field({ nullable: true })
description?: string;
}
次にserviceとresolverを定義します。
task.service.ts
async updateTask(updateTaskInput: UpdateTaskInput): Promise<Task> {
const { id, name, dueDate, status, description } = updateTaskInput;
return await this.prismaService.task.update({
data: {
name,
dueDate,
status,
description,
},
where: { id },
});
}
task.resolver.ts
@Mutation(() => TaskModel)
async updateTask(
@Args('updateTaskInput') updaTaskInput: UpdateTaskInput,
): Promise<Task> {
return await this.taskService.updateTask(updaTaskInput);
}
これで更新ができるようになります。
次に削除機能となります。
task.service.ts
async deleteTask(id: number): Promise<Task> {
return await this.prismaService.task.delete({
where: { id },
});
}
task.resolver.ts
@Mutation(() => TaskModel)
async deleteTask(@Args('id', { type: () => Int }) id: number): Promise<Task> {
return await this.taskService.deleteTask(id);
}
@Mutation(() => TaskModel):
@Mutationデコレータは、このメソッドがGraphQLのミューテーションリゾルバとして動作することを示しています。
() => TaskModelは、このミューテーションがTaskModel型のオブジェクトを返すことを示しています。これは、通常削除操作が成功した場合に削除されたオブジェクトを返す場面でよく見られます。
メソッド定義 async deleteTask(@Args('id', { type: () => Int }) id: number): Promise<Task>:
このメソッドは、タスクを削除するためのもので、非同期的に動作することを示すasyncキーワードが使用されています。
引数@Args('id', { type: () => Int }) id: numberは、クライアントから提供されたid引数を取得しています。このidは、削除するタスクの識別子として使用されます。
type: () => Intは、提供される引数の型が整数であることを示しています。
メソッドの実行内容:
return await this.taskService.deleteTask(id);: このメソッドはTaskServiceのdeleteTaskメソッドを呼び出して、指定されたIDを持つタスクを削除しています。その後、削除されたタスクの情報を返しています(これは多くのAPIで慣例として行われるものです)。
ユーザテーブルの作成
ここからは新規登録、ログインが行えるようにしましょう。
まずschema.prismaを開いてUserモデルを定義してTaskモデルを編集します。
schema.prisma
model Task{
id Int @id @default(autoincrement())
name String @db.VarChar(255)
dueDate String @db.VarChar(10)
status Status @default(NOT_STARTED)
description String?
createdAt DateTime @default(now()) @db.Timestamp(0)
updatedAt DateTime @updatedAt @db.Timestamp(0)
userId Int
user User @relation(fields: [userId],references: [id],onDelete: Cascade)
}
enum Status{
NOT_STARTED
IN_PROGRESS
COMPLETED
}
model User{
id Int @id @default(autoincrement())
name String @db.VarChar(255)
email String @unique @db.VarChar(255)
password String @db.VarChar(255)
createdAt DateTime @default(now()) @db.Timestamp(0)
updatedAt DateTime @updatedAt @db.Timestamp(0)
task Task[]
}
テーブル構造を変更したことでtask.service.tsにエラーが出ているのでまず
createTask.input.tsを編集します。userIdを追加します。
createTask.input.ts
@Field(() => Int)
userId: number;
そして、task.service.tsを開きcreateTask、getTasksメソッドを編集します。
task.service.ts
async getTasks(userId: number): Promise<Task[]> {
return await this.prismaService.task.findMany({
where: { userId },
});
}
async createTask(createTaskInput: CreateTaskInput): Promise<Task> {
const { name, dueDate, description, userId } = createTaskInput;
return await this.prismaService.task.create({
data: {
name,
dueDate,
description,
userId,
},
});
}
getTasksメソッド:
引数:
userId: number: ユーザーのIDを示す数値。このIDに関連付けられたタスクをデータベースから取得するために使用されます。
返り値: Promise<Task[]> - Taskオブジェクトの配列のプロミス。
動作:
this.prismaService.task.findMany({ where: { userId } });: Prismaを使用して、指定されたuserIdを持つ全てのタスクをデータベースから取得します。whereクローズは、特定の条件に一致するレコードのみを取得するためのフィルタとして働きます。
createTaskメソッド:
引数:
createTaskInput: CreateTaskInput: タスク作成のための入力データを含むオブジェクト。これにはタスクの名前、期限、説明、そしてユーザーIDが含まれます。
返り値: Promise<Task> - 作成されたTaskオブジェクトのプロミス。
動作:
const { name, dueDate, description, userId } = createTaskInput;: 入力オブジェクトから必要なプロパティを取り出しています。
this.prismaService.task.create({ data: { name, dueDate, description, userId } });: Prismaを使用して、提供されたデータで新しいタスクをデータベースに作成します。
そしてtask.resolver.tsのgetTasksメソッドを下記のように変更すればエラーは解消されます。
task.resolver.ts
async getTasks(
@Args('userId', { type: () => Int }) userId: number,
): Promise<Task[]> {
return await this.taskService.getTasks(userId);
}
ユーザ作成機能
まずはmodule、resolver、serviceを作成します。ターミナルで以下のコマンドを入力してください。
command
nest g module user
nest g resolver user --no-spec
nest g service user --no-spec
そしてuser.moduleにPrismaをインポートします。
user.module.ts
@Module({
imports: [PrismaModule],
providers: [UserResolver, UserService],
})
次にタスクと同様user配下にmodelフォルダを作成し、その中にuser.model.tsを作成てください。
user.model.ts
import { Field, HideField, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(() => Int)
id: number;
@Field()
name: string;
@Field()
email: string;
@HideField()
createdAt: Date;
@Field()
updatedAt: Date;
}
@ObjectType()デコレータ:
@ObjectType()
このデコレータは、次に定義されるクラスをGraphQLのオブジェクト型として登録します。これにより、GraphQLスキーマにUserという型が追加されることになります。
プロパティとデコレータ:
各プロパティは、@Field()や@HideField()などのデコレータとともに定義されています。
@Field(() => Int): このデコレータは、該当するプロパティがGraphQLスキーマにフィールドとして表示されることを示しています。(() => Int)は、そのフィールドの型がInt(整数)であることを示しています。
@Field(): このデコレータは、該当するプロパティがGraphQLスキーマにフィールドとして表示されることを示しています。型は暗黙的に推論されます(この場合、stringやDate)。
@HideField(): このデコレータは、該当するプロパティがGraphQLスキーマから隠されることを示しています。これにより、createdAtはGraphQLのクエリやレスポンスには表示されません。
次にuser作成時に使用するDTOを作成します。
userディレクトリ配下にdtoフォルダを作成しcreateUser.input.tsを作成してください。
createUser.input.ts
import { Field, InputType } from '@nestjs/graphql';
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
@InputType()
export class CreateUserInput {
@Field()
@IsNotEmpty()
name: string;
@Field()
@IsEmail()
email: string;
@Field()
@MinLength(8)
password: string;
}
次にパスワードをハッシュ化するライブラリをインストールします。
command
npm i bcrypt
npm i --save-dev @types/bcrypt
ではserviceを作成していきます。
user.service.ts
@Injectable()
export class UserService {
constructor(private readonly prismaService: PrismaService) {}
async createUser(createUserInput: CreateUserInput): Promise<User> {
const { name, email, password } = createUserInput;
const hashedPassword = await bcrypt.hash(password, 10);
return await this.prismaService.user.create({
data: {
name,
email,
password: hashedPassword,
},
});
}
}
@Injectable()デコレータ:
typescript
Copy code
@Injectable()
NestJSで提供される@Injectable()デコレータは、その下のクラスがNestJSの依存注入システムによって管理されるサービスまたはプロバイダであることを示します。これにより、他のクラスやモジュールからこのサービスを簡単に注入して利用できるようになります。
コンストラクタと依存注入:constructor(private readonly prismaService: PrismaService) {}
コンストラクタ内でPrismaServiceを注入しています。これにより、このクラスのメソッド内でPrismaのサービスを利用できるようになります。
createUserメソッド:
このメソッドは、新しいユーザーを作成するためのロジックを提供します。
async createUser(createUserInput: CreateUserInput): Promise<User> {...}: メソッドは非同期であり、createUserInputというパラメータを受け取り、User型のプロミスを返すことを示します。
const { name, email, password } = createUserInput;: createUserInputオブジェクトからname, email, passwordを取り出します。
const hashedPassword = await bcrypt.hash(password, 10);: bcryptを使用して、パスワードをハッシュ化します。10は、ハッシュ化の回数(コスト)を示すもので、この値が大きいほどセキュリティは高まりますが、ハッシュ化にかかる時間も増えます。
this.prismaService.user.create({...}): Prismaのサービスを使用して、新しいユーザーをデータベースに保存します。ハッシュ化されたパスワードを保存します。
次にresolverの作成に移ります。
user.resolver.ts
@Resolver()
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Mutation(() => UserModel)
async createUser(
@Args('createUserInput') createUserInput: CreateUserInput,
): Promise<User> {
return await this.userService.createUser(createUserInput);
}
}
@Resolver()デコレータ:
@Resolver()
このデコレータは、NestJSで提供されるもので、その下のクラスがGraphQLのリゾルバクラスであることを示しています。
createUserミューテーション:
このミューテーションは、新しいユーザーを作成するためのロジックを提供します。
@Mutation(() => UserModel): このデコレータは、メソッドがGraphQLのミューテーションであることを示しています。ミューテーションの戻り値としてUserModelを返すことも示しています。
async createUser(@Args('createUserInput') createUserInput: CreateUserInput): Promise<User>: このメソッドは非同期であり、createUserInputという名前の引数を受け取り、User型のプロミスを返すことを示します。
@Args('createUserInput') createUserInput: CreateUserInput: @Argsデコレータは、GraphQLのクエリまたはミューテーションから引数を取得するために使用されます。この場合、createUserInputという名前の引数を取得し、それをCreateUserInput型として解析します。
return await this.userService.createUser(createUserInput);: userServiceのcreateUserメソッドを呼び出し、受け取ったcreateUserInputを引数として渡します。
ユーザ取得機能の実装
次は作成したユーザを取得できるようにしましょう。
user.service.tsを開いてください。
user.service.ts
async getUser(email: string): Promise<User> {
return await this.prismaService.user.findUnique({
where: { email },
});
}
this.prismaService.user.findUnique: prismaServiceは、PrismaというORMを使用してデータベースの操作を行うためのサービスです。この行は、Prismaのuserモデルを使用して、findUniqueというクエリを実行しています。このクエリは、指定された条件に一致するユニークなレコードをデータベースから検索して取得します。
where: { email }: この部分は、検索条件を指定しています。具体的には、emailフィールドがメソッドの引数として提供されたemailと一致するレコードを検索して取得します。
次にdtoを作成します。
getUser.args.ts
import { ArgsType, Field } from '@nestjs/graphql';
import { IsEmail } from 'class-validator';
@ArgsType()
export class GetUserArgs {
@Field()
@IsEmail()
email: string;
}
クエリが@Qureryの時は@ArgsTypeで指定をする必要があります。
次にresolverを定義します。
task.resolver.ts
@Query(() => UserModel, { nullable: true })
async getUser(@Args() getUserArgs: GetUserArgs): Promise<User> {
return await this.userService.getUser(getUserArgs.email);
}
@Query(() => UserModel, { nullable: true })
@Queryデコレータは、このメソッドがGraphQLのクエリを処理するものであることを示します。
() => UserModel は、このクエリが返すデータの型がUserModelであることを示します。
{ nullable: true }は、このクエリの結果としてnullが返ることが許可されていることを示します。これは、指定されたメールアドレスに対応するユーザーが見つからない場合にnullを返すことができることを意味します。
getUserメソッド:
async getUser(@Args() getUserArgs: GetUserArgs): Promise<User> {
return await this.userService.getUser(getUserArgs.email);
}
getUserは非同期のメソッドで、その結果としてPromise<User>を返します。
@Args()デコレータを使用して、クエリの引数を取得します。この例では、GetUserArgs型のgetUserArgsという名前の引数を取得します。
メソッドの中で、this.userService.getUser(getUserArgs.email);を使って、ユーザーサービスから指定されたメールアドレスに対応するユーザー情報を取得します。
ログイン機能
次はログイン機能の作成に入ります。
module、service、resolverを作成します。
html
nest g module auth
nest g resolver auth --no-spec
nest g service auth --no-spec
次に以下のライブラリをインストールします。
command
npm i @nestjs/passport passport passport-local
npm i --save-dev @types/passport-local
npm i @nestjs/jwt passport-jwt
npm i --save-dev @types/passport-jwt
次に.envファイルを開き以下を追加します。
JWT_SECRET="jwt@secret#key"
次に外部からuser.moduleを扱えるようにするためにuser.module.tsでUserServiceをexportsします。
user.module.ts
@Module({
imports: [PrismaModule],
providers: [UserResolver, UserService],
exports: [UserService],
})
次にauth.module.tsに認証関係のライブラリをインストールします。
auth.module.ts
@Module({
imports: [
UserModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthResolver, AuthService],
})
PassportModule: NestJSが提供するPassport.jsベースの認証モジュールです。ここでdefaultStrategyにjwtを指定して、JWT認証をデフォルトのストラテジーとして設定しています。
JwtModule: JWT認証に関する機能を提供するモジュールです。ここでregisterメソッドを使って、JWTの設定を行っています。
secret: JWTの署名に使用される秘密鍵。環境変数から取得。
signOptions: JWTの署名オプション。この場合、JWTの有効期限が1時間。
次にdtoを作成します。
authディレクトリにdtoフォルダを作りsignIn.input.tsとsignInResponse.tsを作成します。
signIn.input.ts
import { Field, InputType } from '@nestjs/graphql';
import { IsEmail, MinLength } from 'class-validator';
@InputType()
export class SignInInput {
@Field()
@IsEmail()
email: string;
@Field()
@MinLength(8)
password: string;
}
signInResponse.ts
import { Field, ObjectType } from '@nestjs/graphql';
import { User } from 'src/user/model/user.model';
@ObjectType()
export class SignInResponse {
@Field()
accessToken: string;
@Field(() => User)
user: User;
}
@ObjectType() デコレータは、このクラスがGraphQLのオブジェクトタイプであることを示します。これにより、GraphQLスキーマでこのクラスをオブジェクトタイプとして使用できるようになります。
SignInResponse クラス:export class SignInResponse {
@Field()
accessToken: string;
@Field(() => User)
user: User;
}
SignInResponse クラスは、サインイン時のレスポンスを表す型定義を持っています。
@Field() デコレータは、そのフィールドがGraphQLスキーマの一部であることを示しています。
accessToken: このフィールドは、サインイン成功時に返されるアクセストークンを表します。
@Field(() => User) デコレータは、このフィールドがUser型を持つことを示しています。このフィールドは、サインインしたユーザーの情報を返すためのものです。
次にauth配下にtypesディレクトリを作り、jwtPayload.type.tsを作成します。
jstPayload.type.ts
export type JwtPayload = {
email: string;
sub: number;
};
その次にauth.service.tsを作成します。
auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '@prisma/client';
import { UserService } from 'src/user/user.service';
import * as bcrypt from 'bcrypt';
import { SignInResponse } from './dto/signInResponse';
import { JwtPayload } from './types/jwtPayload.type';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.userService.getUser(email);
if (user && (await bcrypt.compare(password, user.password))) {
return user;
}
return null;
}
async signIn(user: User): Promise<SignInResponse> {
const payload: JwtPayload = { email: user.email, sub: user.id };
return { accessToken: this.jwtService.sign(payload), user };
}
次にauthディレクトリの中にstrategiesディレクトリを作りその中にlocal.strategy.tsを作成します。
local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
import { User } from 'src/user/model/user.model';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({ usernameField: 'email' });
}
// 名前がvalidateである必要がある
async validate(email: string, password: string): Promise<User> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
@Injectable()デコレータ:
このクラスがNestJSの依存注入システムによって注入可能であることを示しています。
LocalStrategyクラス:
PassportStrategyを継承し、PassportのStrategy(ローカル認証ストラテジー)を使っています。
コンストラクタ:
AuthServiceを注入して、ユーザーの認証処理を行います。
super({ usernameField: 'email' }); は、PassportStrategyの基底クラスのコンストラクタを呼び出すもので、ローカル認証においてユーザー名のフィールドとしてemailを使用することを示しています。
validateメソッド:
Passportローカルストラテジーはvalidateメソッドを求めます。このメソッドは、認証の際に呼び出されます。
validateメソッドは、AuthServiceのvalidateUserメソッドを使用してユーザーの認証を試みます。
もしvalidateUserが認証されたユーザーを返さない場合、UnauthorizedExceptionが投げられ、認証が失敗したことを示します。
認証に成功した場合、ユーザーの情報が返されます。この情報は、後続のリクエスト処理で使用されます。
そしてauth.module.tsに今作ったStrategyを登録します。
auth.module.ts
@Module({
imports: [
UserModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthResolver, AuthService, LocalStrategy],
})
次はGuardsを作成します。
authディレクトリにguardsディレクトリを作成してgql-auth.guard.tsを作成してください。
gql-auth.guard.ts
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
export class GqlAuthGuard extends AuthGuard('local') {
constructor() {
super();
}
// 名前は変えてはダメ
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext();
request.body = ctx.getArgs().signInInput;
return request;
}
}
GqlAuthGuardクラス:
このクラスはNestJSのAuthGuardを拡張しており、認証ガードのカスタマイズを行っています。カスタマイズする理由は、GraphQLのコンテクストは通常のHTTPリクエストとは異なるため、その差異を吸収する必要があるからです。
コンストラクタ:
super();は、基底クラス(AuthGuard)のコンストラクタを呼び出します。
'local'は、使用するPassportのストラテジーを示しています。この場合、ローカルストラテジーを使うことを示しています。
getRequestメソッド:
このメソッドは、AuthGuardのgetRequestメソッドをオーバーライド(上書き)しています。
通常のHTTPリクエストのコンテクストとGraphQLのコンテクストを橋渡しするためのロジックが含まれています。
GqlExecutionContext.create(context);で、通常のNestJSのExecutionContextをGraphQLのコンテクストに変換しています。
ctx.getContext();で、実際のGraphQLの実行コンテクストを取得しています。
ctx.getArgs().signInInput;で、GraphQLのリクエストからsignInInput引数を取得しています。
最後に、HTTPリクエストのbodyプロパティにsignInInputをセットして、リクエストオブジェクトを返しています。これにより、Passportのローカルストラテジーがこの入力を使用して認証を試みることができます。
次にresolverです。
auth.resolver.ts
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { AuthService } from './auth.service';
import { SignInResponse } from './dto/signInResponse';
import { UseGuards } from '@nestjs/common';
import { GqlAuthGuard } from './guards/gql-auth.guard';
import { SignInInput } from './dto/signIn.input';
@Resolver()
export class AuthResolver {
constructor(private readonly authService: AuthService) {}
@Mutation(() => SignInResponse)
@UseGuards(GqlAuthGuard)
// gqlAuthGuardのsignInInputと合わせる必要がある
async signIn(
@Args('signInInput') signInInput: SignInInput,
@Context() context: any,
) {
return await this.authService.signIn(context.user);
}
}
@Resolver()デコレータ:
AuthResolverクラスがGraphQLのリゾルバであることを示しています。
コンストラクタ:
AuthServiceをインジェクションし、クラスの中で利用可能にしています。
signInメソッド:
ユーザーのログイン機能を提供するためのメソッドです。
@Mutation(() => SignInResponse)デコレータ:
このメソッドがGraphQLのmutationであることを示しており、返り値の型としてSignInResponseを指定しています。
@UseGuards(GqlAuthGuard)デコレータ:
GqlAuthGuardをこのメソッドに適用しています。このガードがアクティブになると、リクエストがこのメソッドに到達する前に認証処理が行われることになります。
async signIn(@Args('signInInput') signInInput: SignInInput, @Context() context: any):
@Args('signInInput')デコレータは、GraphQLのリクエストからsignInInputという名前の引数を取得します。この引数はSignInInput型として定義されています。
@Context()デコレータは、実行中のGraphQLリクエスト/レスポンスのコンテクストを取得するために使用されます。このコンテクストの中には、認証情報やセッション情報など、リクエストに関連する多くの情報が含まれています。
return await this.authService.signIn(context.user);:
context.userは、GqlAuthGuardによって認証されたユーザー情報を含むオブジェクトです。この情報をsignInメソッドに渡して、ログイン処理を完了させています
Jwt認証
最後に認証済みユーザのみタスクの追加を行えるようにしましょう。
strategiesディレクトリ内にjwt.strategy.tsを作成してください。
jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from 'src/user/user.service';
import { JwtPayload } from '../types/jwtPayload.type';
import { User } from '@prisma/client';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly userService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: JwtPayload): Promise<User | null> {
return await this.userService.getUser(payload.email);
}
PassportStrategy(Strategy):
このクラスは、passport-jwtのストラテジを拡張しています。これにより、JWT認証の実装をこのクラス内で行います。
コンストラクタ:
UserServiceをインジェクションしています。これにより、ユーザー関連の操作を行うためのサービスがこのクラス内で利用可能になります。
super関数を使って、JWTストラテジのオプションを指定しています。これには、トークンの取得方法、有効期限の無視設定、JWTの秘密キーなどが含まれています。
validateメソッド:
このメソッドは、PassportStrategyによって要求されるもので、JWTがデコードされた後に呼び出されます。メソッドの主要な役割は、ペイロードを検証し、それに基づいてユーザーを返すことです。
ここでは、ペイロードからのemailを使って、該当するユーザーをデータベースから取得しています。
次にauth.module.tsのprovidersに追加します。
providers: [AuthResolver, AuthService, LocalStrategy, JwtStrategy],
次にGuardsの作成です。
guardsディレクトリ以下にjwt-auth-guard.tsを作成してください。
jwt-auth.guard.ts
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
export class JwtAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
JwtAuthGuardクラス:
このクラスは、NestJSのAuthGuardを拡張しています。'jwt'という文字列をAuthGuardのコンストラクタに渡しているのは、passportのJWTストラテジを使用することを示しています。
getRequestメソッド:
このメソッドは、AuthGuard内部でリクエストオブジェクトを取得するために使用されます。デフォルトでは、通常のHTTPリクエストを扱うためのものですが、GraphQLを使用している場合、リクエストオブジェクトの取得方法が異なります。
ここでGqlExecutionContext.create(context)を使用して、通常のExecutionContextからGraphQL固有のコンテキストを生成しています。
ctx.getContext().reqを返すことで、実際のHTTPリクエストオブジェクトを返しています。これにより、AuthGuardはJWTを検証するための情報(例えばヘッダーのAuthorizationフィールドなど)にアクセスできます。
今作ったGuardを適用させます。
まずはuser.resolver.tsです。
user.resolver.ts
@Query(() => UserModel, { nullable: true })
@UseGuards(JwtAuthGuard)
async getUser(@Args() getUserArgs: GetUserArgs): Promise<User> {
return await this.userService.getUser(getUserArgs.email);
}
次にtask.resolver.tsです
task.resolver.ts
export class TaskResolver {
constructor(private readonly taskService: TaskService) {}
@Query(() => [TaskModel], { nullable: 'items' })
@UseGuards(JwtAuthGuard)
async getTasks(
@Args('userId', { type: () => Int }) userId: number,
): Promise<Task[]> {
return await this.taskService.getTasks(userId);
}
@Mutation(() => TaskModel)
@UseGuards(JwtAuthGuard)
async createTask(
@Args('createTaskInput') creteTaskInput: CreateTaskInput,
): Promise<Task> {
return await this.taskService.createTask(creteTaskInput);
}
@Mutation(() => TaskModel)
@UseGuards(JwtAuthGuard)
async updateTask(
@Args('updateTaskInput') updaTaskInput: UpdateTaskInput,
): Promise<Task> {
return await this.taskService.updateTask(updaTaskInput);
}
@Mutation(() => TaskModel)
@UseGuards(JwtAuthGuard)
async deleteTask(@Args('id', { type: () => Int }) id: number): Promise<Task> {
return await this.taskService.deleteTask(id);
}
これでTokenを持ってないユーザはアクセスできないようになりました。