💬

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

2023/04/13に公開

NestJSPrismaと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/web3ten0/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が結構便利なのかなと思っていて、下記でやってみたりしました。

https://zenn.dev/web3ten0/articles/d0417c9c10ec04

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

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

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

Discussion