NestJS + Prisma + GraphQL + Passportの始め方のメモ
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.prisma
にmodel
を追加することで、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.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ファイルを作成するようにできます。
// 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.ts
にautoSchemaFile
を設定しました。また、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.ts
とupdate-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のimports
にPrismaModule
を追加します。
// 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とやることは同じなので割愛します。全体のコードは下記にありますので、よかったら参考にしてください。
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を確認するようにしています。ここを参考にしました。
Discussion