NestJS + Prisma + GraphQL + Passportの始め方のメモ
NestJSとPrismaと PostgreSQL を使ってブログっぽいものを作ります。NestJS で開発するときの始め方のメモです。GraphQL(コードファースト)も使います。
NestJS のプロジェクトを作成
下記で作成されます。今回は npm を使います。
nest new cmsPrisma を入れます
下記で prisma を入れて、初期化します。npx prisma initは--datasource-providerオプションをつけられます。これをsqliteなどにすると、それに合わせて初期化されます。--datasource-providerオプションのデフォルトはpostgresqlです。
npm i -D prismanpx prisma initPostgreSQL を設定します
とりあえずローカルでの開発を進めます。docker-compose で PostgreSQL を用意します。
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.prismaにmodelを追加することで、migrate するとテーブルにmodelの内容が反映されます。合わせて migration ファイルも作成されます。今回は暫定的な内容として、ブログ記事を表すPostと、投稿者を表すUserを追加しました。
...
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 initGraphQL を入れます
NestJS で GraphQL を使う際の説明はここにあります。まずは、GraphQL 関連をインストールします。
npm i @nestjs/graphql @nestjs/apollo @apollo/server graphqlCLI プラグインを設定します
CLI プラグインを有効にすると、コードを書く量を減らせます。詳細はこちらをご確認ください。
CLI プラグインを有効にするには、プロジェクトルートにある、nest-cli.jsonのpluginsに@nestjs/graphqlを追加します。
{ "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, "plugins": ["@nestjs/graphql"] }}コードファースト前提で GraphQL を設定します
まずは、app.module.tsのimportsにGraphQLModuleを追加します。その際にオプションで、autoSchemaFileを追加します。これを追加すると、モデルを定義したファイルに適当なデコレータを付与することで、自動的に gql ファイルを作成するようにできます。
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 usersnest 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) WebSocketsstart:dev を実行して schema.gql を作成してみる
下記コマンドで、NestJS アプリが起動します。:devをつけると、コードの変更がある度にホットリロードされます。
npm run start:dev先程、src/app.module.tsにautoSchemaFileを設定しました。また、nest g resource postsを実行した際に、src/posts/entities/post.entity.tsが自動生成されているかと思います。このファイルに、Post モデルの構造(型)を書き、各フィールドに適切なデコレータを付与すると、start:devを実行した際等に、autoSchemaFileで設定した場所に、自動的にschema.gqlが作成されます。
現在は、自動生成した状態のままなので、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 が下記を作成してくれました。
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が設定されます。
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.tsとupdate-post.input.tsも修正します。Create 時は、とりあえず、タイトルとコンテンツのみ受け取り、後は自動でデフォルト値あるいは認証ユーザの ID が保存されるものとします。また、Update 時はタイトル、コンテンツと記事 ID を受け取るものとします。
import { InputType } from '@nestjs/graphql';
@InputType()export class CreatePostInput { title: string; content?: string;}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-prismaposts.module.ts のimportsにPrismaModuleを追加します。
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 のコードを書きます。
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 を確認してみます。下記のようになっていました。便利ですね。
# ------------------------------------------------------# 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 { id email name }}上記を実行して成功したら、下記のようなレスポンスがあります。
{ "data": { "createUser": { "id": "2e72a8e3-db17-403b-815a-eb4871adb093", "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 を確認するようにしています。ここを参考にしました。