NestJS + Prisma + GraphQL + Passportの始め方のメモ

2023/04/13

NestJS Prisma と PostgreSQL を使ってブログっぽいものを作ります。NestJS で開発するときの始め方のメモです。GraphQL(コードファースト)も使います。

目次

NestJS のプロジェクトを作成

下記で作成されます。今回は npm を使います。

nest new cms

Prisma を入れます

下記で prisma を入れて、初期化します。npx prisma init--datasource-providerオプション をつけられます。これをsqliteなどにすると、それに合わせて初期化されます。--datasource-providerオプションのデフォルトはpostgresqlです。

npm i -D prisma
npx prisma init

PostgreSQL を設定します

とりあえずローカルでの開発を進めます。docker-compose で PostgreSQL を用意します。

docker-compose.yml
version: '3.9'
services:
  db:
    image: postgres:13
    container_name: cms-postgres
    restart: always
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydatabase
    ports:
      - '5432:5432'
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

上記はプロジェクトルートに配置して、プロジェクトルートで下記を実行します。

docker-compose up

.env の DB の URL を設定します

prisma initの際に、自動的に.envファイルが作成されます。.envファイル内のDATABASE_URLを上記のdocker-compose.ymlの内容に合わせて修正します。

DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/cms-postgres?schema=public"

schema.prisma に model を追加して migrate します

schema.prismamodelを追加することで、migrate するとテーブルにmodelの内容が反映されます。合わせて migration ファイルも作成されます。今回は暫定的な内容として、ブログ記事を表すPostと、投稿者を表すUserを追加しました。

prisma/schema.prisma
...

model Post {
  id        String   @id @default(uuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
}

model User {
  id    String  @id @default(uuid())
  email String  @unique
  name  String?
  posts Post[]
}

上記を追加したら、下記コマンドで migrate します。

npx prisma migrate dev --name init

GraphQL を入れます

NestJS で GraphQL を使う際の説明はここ にあります。まずは、GraphQL 関連をインストールします。

npm i @nestjs/graphql @nestjs/apollo @apollo/server graphql

CLI プラグインを設定します

CLI プラグインを有効にすると、コードを書く量を減らせます。詳細はこちら をご確認ください。

CLI プラグインを有効にするには、プロジェクトルートにある、nest-cli.jsonplugins@nestjs/graphqlを追加します。

{
	"$schema": "https://json.schemastore.org/nest-cli",
	"collection": "@nestjs/schematics",
	"sourceRoot": "src",
	"compilerOptions": {
		"deleteOutDir": true,
		"plugins": ["@nestjs/graphql"]
	}
}

コードファースト前提で GraphQL を設定します

まずは、app.module.tsimportsGraphQLModuleを追加します。その際にオプションで、autoSchemaFileを追加します。これを追加すると、モデルを定義したファイルに適当なデコレータを付与することで、自動的に gql ファイルを作成するようにできます。

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
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')
		})
	],
	controllers: [AppController],
	providers: [AppService]
})
export class AppModule {}

User と Post のリソースを自動作成します

NestJS はリソースを自動作成できます。しかも GraphQL の利用を前提としたリソース作成が可能です。下記のようにやります。

nest g resource users
nest g resource posts

上記の nest g resource users を実行すると、下記のように GrahpQL が選択できますので、GraphQL(code first)を選択します。

❯ nest g resource users
? What transport layer do you use?
  REST API
❯ GraphQL (code first)
  GraphQL (schema first)
  Microservice (non-HTTP)
  WebSockets

start:dev を実行して schema.gql を作成してみる

下記コマンドで、NestJS アプリが起動します。:devをつけると、コードの変更がある度にホットリロードされます。

npm run start:dev

先程、src/app.module.tsautoSchemaFileを設定しました。また、nest g resource postsを実行した際に、src/posts/entities/post.entity.tsが自動生成されているかと思います。このファイルに、Post モデルの構造(型)を書き、各フィールドに適切なデコレータを付与すると、start:devを実行した際等に、autoSchemaFileで設定した場所に、自動的にschema.gqlが作成されます。

現在は、自動生成した状態のままなので、schema.gqlは下記のような内容になっているかと思います。

src/schema.gql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------

type User {
	"""
	Example field (placeholder)
	"""
	exampleField: Int!
}

type Post {
	"""
	Example field (placeholder)
	"""
	exampleField: Int!
}

type Query {
	users: [User!]!
	user(id: Int!): User!
	posts: [Post!]!
	post(id: Int!): Post!
}

type Mutation {
	createUser(createUserInput: CreateUserInput!): User!
	updateUser(updateUserInput: UpdateUserInput!): User!
	removeUser(id: Int!): User!
	createPost(createPostInput: CreatePostInput!): Post!
	updatePost(updatePostInput: UpdatePostInput!): Post!
	removePost(id: Int!): Post!
}

input CreateUserInput {
	"""
	Example field (placeholder)
	"""
	exampleField: Int!
}

input UpdateUserInput {
	"""
	Example field (placeholder)
	"""
	exampleField: Int
	id: Int!
}

input CreatePostInput {
	"""
	Example field (placeholder)
	"""
	exampleField: Int!
}

input UpdatePostInput {
	"""
	Example field (placeholder)
	"""
	exampleField: Int
	id: Int!
}

Post の Entity を完成させます

現時点の Post の DB テーブルの構造に合わせて、post.entity.tsを修正します。schema.prisma の Post モデルをコメントとして貼り付けると、自動で Github Copilot が下記を作成してくれました。

src/posts/entities/post.entity.ts
import { ObjectType, Field, Int } from '@nestjs/graphql';

// model Post {
//   id        String   @id @default(uuid())
//   createdAt DateTime @default(now())
//   updatedAt DateTime @updatedAt
//   title     String
//   content   String?
//   published Boolean  @default(false)
//   author    User     @relation(fields: [authorId], references: [id])
//   authorId  String
// }

@ObjectType()
export class Post {
	@Field(() => String)
	id: string;

	@Field(() => Date)
	createdAt: Date;

	@Field(() => Date)
	updatedAt: Date;

	@Field(() => String)
	title: string;

	@Field(() => String, { nullable: true })
	content?: string;

	@Field(() => Boolean)
	published: boolean;

	@Field(() => String)
	authorId: string;
}

上記のままで問題ないのですが、先程、nest-cli.json@nestjs/graphqlプラグインの利用を設定しました。このプラグインの説明はここ にありますが、基本的に@Fieldを勝手につけてくれます。この方がシンプルになりますので、不要な@Fieldを削除してみます。尚、content?のように?がついている場合は、自動的にnullable:trueが設定されます。

src/posts/entities/post.entity.ts
import { ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Post {
	id: string;
	createdAt: Date;
	updatedAt: Date;
	title: string;
	content?: string;
	published: boolean;
	authorId: string;
}

上記と同じ要領で、src/posts/dto/create-post.input.tsupdate-post.input.tsも修正します。Create 時は、とりあえず、タイトルとコンテンツのみ受け取り、後は自動でデフォルト値あるいは認証ユーザの ID が保存されるものとします。また、Update 時はタイトル、コンテンツと記事 ID を受け取るものとします。

src/posts/dto/create-post.input.ts
import { InputType } from '@nestjs/graphql';

@InputType()
export class CreatePostInput {
	title: string;
	content?: string;
}
src/posts/dto/update-post.input.ts
import { CreatePostInput } from './create-post.input';
import { InputType, PartialType } from '@nestjs/graphql';

@InputType()
export class UpdatePostInput extends PartialType(CreatePostInput) {
	id: string;
}

Post の resolver を微調整します

今回Post.idは UUID(string)です。nest g resource postsにより、自動生成されたpost.resolver.tsは、全体的にidが Int 型の想定になっています。これらを修正します。

import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
...

@Resolver(() => Post)
export class PostsResolver {
  ...

  @Query(() => Post, { name: 'post' })
  findOne(@Args('id', { type: () => String }) id: string) {
    return this.postsService.findOne(id);
  }

  ...

  @Mutation(() => Post)
  removePost(@Args('id', { type: () => String }) id: string) {
    return this.postsService.remove(id);
  }
}

Post の Service を作成します

posts.service.ts に、Prisma による CURD の処理を書きます。そのためには、PrismaService を作る必要がありまして、作成方法がここ に書いてあります。ただ、nestjs-prisma というライブラリがありまして、これを使うと、自分で PrismaService を作らなくてよくなります。今回はこれを使ってみます。

npm i nestjs-prisma

posts.module.ts のimportsPrismaModuleを追加します。

src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsResolver } from './posts.resolver';
import { PrismaModule } from 'nestjs-prisma';

@Module({
	imports: [PrismaModule],
	providers: [PostsResolver, PostsService]
})
export class PostsModule {}

posts.service.ts に PrismaService を使った CURD のコードを書きます。

src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { CreatePostInput } from './dto/create-post.input';
import { UpdatePostInput } from './dto/update-post.input';
import { PrismaService } from 'nestjs-prisma';

@Injectable()
export class PostsService {
	constructor(private readonly prisma: PrismaService) {}

	create(createPostInput: CreatePostInput) {
		const authorId = 'dummy-id';
		return this.prisma.post.create({
			data: {
				...createPostInput,
				author: {
					connect: { id: authorId }
				}
			}
		});
	}

	findAll() {
		return this.prisma.post.findMany();
	}

	findOne(id: string) {
		return this.prisma.post.findUnique({
			where: { id }
		});
	}

	update(id: string, updatePostInput: UpdatePostInput) {
		return this.prisma.post.update({
			where: { id },
			data: updatePostInput
		});
	}

	remove(id: string) {
		return this.prisma.post.delete({
			where: { id }
		});
	}
}

User の Entity や Service も完成させます

上記の Post とやることは同じなので割愛します。全体のコードは下記にありますので、よかったら参考にしてください。

https://github.com/edo1z/nestjs-graphql-passport-sample

schema.gql を確認してみます

Entity や DTO などを修正したので、現時点の schema.gql を確認してみます。下記のようになっていました。便利ですね。

src/schema.gql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------

type User {
	id: String!
	email: String!
	name: String
}

type Post {
	id: String!
	createdAt: DateTime!
	updatedAt: DateTime!
	title: String!
	content: String
	published: Boolean!
	authorId: String!
}

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

type Query {
	users: [User!]!
	user(id: String!): User!
	posts: [Post!]!
	post(id: String!): Post!
}

type Mutation {
	createUser(createUserInput: CreateUserInput!): User!
	updateUser(updateUserInput: UpdateUserInput!): User!
	removeUser(id: String!): User!
	createPost(createPostInput: CreatePostInput!): Post!
	updatePost(updatePostInput: UpdatePostInput!): Post!
	removePost(id: String!): Post!
}

input CreateUserInput {
	email: String!
	name: String
}

input UpdateUserInput {
	email: String
	name: String
	id: String!
}

input CreatePostInput {
	title: String!
	content: String
	authorId: String!
}

input UpdatePostInput {
	title: String
	content: String
	authorId: String
	id: String!
}

GraphQL の Playground でデータを追加してみます

下記にアクセスすると Playground が開きます。

http://localhost:3000/graphql

まずは User を追加します。

mutation {
	createUser(createUserInput: { email: "hoge@example.com", name: "Hoge Taro" }) {
		id
		email
		name
	}
}

上記を実行して成功したら、下記のようなレスポンスがあります。

{
  "data": {
    "createUser": {
      "id": "2e72a8e3-db17-403b-815a-eb4871adb093",
      "email": "hoge@example.com",
      "name": "Hoge Taro"
    }
  }
}

次に Post を追加します。上記のレスポンスの UserID を使います。

mutation {
	createPost(
		createPostInput: {
			title: "Sample Post"
			content: "Hello world!"
			authorId: "2e72a8e3-db17-403b-815a-eb4871adb093"
		}
	) {
		id
		title
		authorId
		createdAt
	}
}

成功したら下記のようなレスポンスがきます。

{
  "data": {
    "createPost": {
      "id": "17964948-6298-4dc5-8205-f381b41b14e9",
      "title": "Sample Post",
      "authorId": "2e72a8e3-db17-403b-815a-eb4871adb093",
      "createdAt": "2023-04-08T05:12:37.612Z"
    }
  }
}

次に、Post を修正してみましょう。

mutation {
	updatePost(updatePostInput: { id: "17964948-6298-4dc5-8205-f381b41b14e9", title: "Hoge Post" }) {
		id
		title
		content
		authorId
	}
}

成功したら下記のようなレスポンスが来ます。

{
  "data": {
    "updatePost": {
      "id": "17964948-6298-4dc5-8205-f381b41b14e9",
      "title": "Hoge Post",
      "content": "Hoge world!!",
      "authorId": "2e72a8e3-db17-403b-815a-eb4871adb093"
    }
  }
}

次に Post を削除してみましょう。

mutation {
	removePost(id: "17964948-6298-4dc5-8205-f381b41b14e9") {
		id
		title
	}
}

成功したら下記のようなレスポンスがきます。

{
  "data": {
    "removePost": {
      "id": "17964948-6298-4dc5-8205-f381b41b14e9",
      "title": "Hoge Post"
    }
  }
}

Prisma Studio でデータを確認してみます

下記を実行すると、studio が起動します。

npx prisma studio

起動すると、下記で確認できるようになります。

http://localhost:5555

認証の仕組みをつくります

これで一応基本的に CURD が出来ましたので、次にユーザの認証関連を作ってみます。フロントも一緒に作る場合で、フロントが Next.js の場合等は、NextAuth が結構便利なのかなと思っていて、ここ でやってみたりしました。

今回は、ヘッドレス API を作るイメージで、認証の仕組みも完全にバックエンドに持ってくる想定です。そのため、今回は Passport を使ってみます。

下記は GraphQL の Mutation でログイン(email + password)出来るようにして、ログイン出来たら JWT が発行されて、Profile 画面など認証が必要な場合は、リクエストヘッダの JWT を確認するようにしています。ここ を参考にしました。

https://github.com/edo1z/nestjs-graphql-passport-sample

Rust🦀, Network⚡, PostgreSQL🐘, Unity🎮

Tags

rust  (9)
rocket  (7)
svelte  (5)
c++  (4)
vscode  (3)
sqlx  (3)
glfw  (2)
opengl  (2)
nestjs  (2)
render  (2)
wsl2  (2)
goerli  (1)
geth  (1)
nft  (1)
gui  (1)
tetris  (1)
jwt  (1)
prisma  (1)
urql  (1)
mdsvex  (1)
tmux  (1)
nvim  (1)
axum  (1)
vim  (1)
pacman  (1)
Cursor  (1)
VSCode  (1)
PHP  (1)