🏛️
【Graphql】Pothosでスキーマ定義&動作確認
はじめに
仕事ではGraphqlのスキーマ定義にtypegraphql-prismaを使用しています。
typegraphql-prismaはgeneratorによる自動生成が便利ですが、Prisma v6以降に対応していないようです。
代替手段を探しているとPothosというライブラリを見つけたので、軽くスキーマ定義から動作確認まで行いました。
※Pothosの使い方紹介がメインなのでPrismaやGraphql周りの説明は省いています
環境
| ライブラリ | バージョン |
|---|---|
| @apollo/server | ^5.0.0 |
| @pothos/core | ^4.10.0 |
| @pothos/plugin-prisma | ^4.14.1 |
| @prisma/adapter-pg | ^7.0.0 |
| @prisma/client | ^6.18.0 |
| apollo-server | ^3.13.0 |
| graphql | ^16.12.0 |
| graphql-scalars | ^1.25.0 |
| prisma | ^6.18.0 |
| tsx | ^4.20.6 |
Pothosのインストール
npm install --save @pothos/plugin-prisma
schema.prismaの設定
schema.prismaにpothosのgeneratorを定義します。
prisma generateを実行するとoutputで指定したパスに型定義ファイルが出力されます。
clientOutputにはprisma-clientの出力先を指定します。
prisma-clientの出力先はpothosのoutputファイルを基準として指定しなければならないことに注意してください。
schema.prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
generator pothos {
provider = "prisma-pothos-types"
clientOutput = "./prisma"
output = "../src/generated/pothos-prisma-types.ts"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Genre {
id Int @id @default(autoincrement())
name String @unique
description String
createdAt DateTime @map("created_at") @default(now())
updatedAt DateTime? @map("updated_at")
}
Pothosを使ったスキーマ定義
定義しなければならない概念は以下の4つです。
- Object
- InputType
- Resolver
- Service
フォルダ構成は以下の通りです。
/
├─ src
│ ├─ prisma
│ ├─ schema
│ │ ├─ common
│ │ │ ├─ SortOrder.ts
│ │ │ └─ StringFilter.ts
│ │ ├─ inputs
│ │ │ ├─ GenreAddInput.ts
│ │ │ ├─ GenreOrderByInput.ts
│ │ │ └─ GenreWhereInput.ts
│ │ ├─ objects
│ │ │ └─ Genre.ts
│ │ ├─ resolvers
│ │ │ └─ GenreResolver.ts
│ │ ├─ services
│ │ │ └─ GenreService.ts
│ │ ├─ builder.ts
│ │ └─ index.ts
│ └─ server.ts
├─ package.json
└─ ・・・
Common
SortOrder.ts
import { builder } from '../builder'
export const SortOrder = builder.enumType('SortOrder', {
values: ['asc', 'desc'] as const,
})
StringFilter.ts
import { builder } from '../builder'
export const StringFilter = builder.inputType('StringFilter', {
fields: (t) => ({
contains: t.string(),
startsWith: t.string(),
endsWith: t.string(),
}),
})
Object
prismaで定義したmodelをここで定義します。この記述はgraphqlのtypeに相当します。
Genre.ts
import { builder } from '../builder'
builder.prismaObject('Genre', {
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
description: t.exposeString('description', {
nullable: true,
}),
}),
})
InputType
graphqlのinputに相当する設定を行います。
GenreAddInput.ts
import { builder } from '../builder'
export const GenreAddInput = builder.inputType('GenreAddInput', {
fields: (t) => ({
name: t.string({ required: true }),
description: t.string({ required: false }),
}),
})
GenreOrderByInput.ts
import { builder } from '../builder'
export const GenreOrderByInput = builder.inputType('GenreOrderByInput', {
fields: (t) => ({
name: t.field({ type: 'SortOrder', required: false }),
id: t.field({ type: 'SortOrder', required: false }),
}),
})
GenreWhereInput.ts
import { builder } from '../builder'
import { StringFilter } from '../common/StringFilter'
export const GenreWhereInput = builder.inputType('GenreWhereInput', {
fields: (t) => ({
name: t.field({ type: StringFilter, required: false }),
}),
})
Resolver
GenreResolver.ts
import { builder } from '../builder'
import { GenreService } from '../services/GenreService'
import { GenreAddInput } from '../inputs/GenreAddInput'
import { GenreWhereInput } from '../inputs/GenreWhereInput'
import { GenreOrderByInput } from '../inputs/GenreOrderByInput'
builder.queryFields((t) => ({
findManyGenres: t.prismaField({
type: ['Genre'],
args: {
where: t.arg({ type: GenreWhereInput, required: false }),
skip: t.arg.int({ required: false }),
take: t.arg.int({ required: false }),
orderBy: t.arg({ type: GenreOrderByInput, required: false }),
},
resolve: async (query, _root, args, ctx) => {
const service = new GenreService(ctx.client)
return service.findMany({
...query,
where: args.where,
skip: args.skip,
take: args.take,
orderBy: args.orderBy,
})
},
}),
}))
builder.mutationFields((t) => ({
addGenre: t.prismaField({
type: 'Genre',
args: {
data: t.arg({ type: GenreAddInput, required: true }),
},
resolve: async (query, _root, args, ctx) => {
const service = new GenreService(ctx.client)
return service.addOneGenre({
data: args.data,
})
},
}),
}))
Service
GenreService.ts
import { PrismaClient } from '../../generated/prisma/client.js'
export class GenreService {
constructor(private client: PrismaClient) {}
async findMany(args: {
where?: { name?: string }
skip?: number
take?: number
orderBy?: any
}) {
return this.client.genre.findMany(args)
}
async addOneGenre(condition: {
data: { name: string; description?: string }
}) {
return this.client.genre.create({
data: {
name: condition.data.name,
description: condition.data.description ?? '',
},
})
}
}
builder.ts
builder.ts
import SchemaBuilder from '@pothos/core'
import PrismaPlugin from '@pothos/plugin-prisma'
import type PrismaTypes from '@pothos/plugin-prisma/generated'
import { PrismaClient } from '../generated/prisma/client.js'
const prisma = new PrismaClient()
export const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes
Context: {
prisma: PrismaClient
}
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
},
});
index.ts
index.ts
import { builder } from './builder.js'
builder.queryType({})
builder.mutationType({})
import './common/SortOrder.js'
import './objects/Genre.js'
import './resolvers/GenreResolver.js'
export const schema = builder.toSchema()
動作確認
サーバ起動
server.ts
import 'dotenv/config';
import "reflect-metadata";
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { PrismaClient } from "./generated/prisma/client.js";
import { schema as pothosSchema } from "./schema/index.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const client = new PrismaClient();
// Pothosスキーマ作成
const schema = pothosSchema;
const server = new ApolloServer({
schema,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({
...req,
client
})
});
console.log(`🚀 Server ready at: ${url}`)
npx tsx ./src/local.ts
プレイグラウンドでクエリ実行

おわりに
typegraphqlはデコレーションで設定しますが、pothosではビルダーバターンを使い関数で設定するという違いがありそうです。
既にデータクラスが用意されているプロジェクトではデコレーションをつけるだけのtypegraphqlの方が早く導入できそうです。それに比べpothosは手書きの手間はかかるものの、Prismaの型定義をそのまま参照しているので型安全性が高そうです。
まだ理解が浅いので詳しい違いを今後記事にしていけたらと思います。
Discussion