フロントエンドエンジニアのブログ

フロント界隈の技術ブログです。

prisma + nexus + graphql-codegen + typescript で コードを自動生成して爆速開発(型情報おまけ付き)

はじめに

https://www.prisma.io/

prisma は 従来の ORM を置き換える graphql 形式の ORM で prisma 独自のスキーマを定義することにより RDBスキーマと graphql 形式のタイプセーフな ORM api が自動生成されます。

フロントエンドの身としてはバックエンドは出来るだけ意識せずにプロダクトを作っていきたいと考えています。

今回はこの prisma 本体と nexus, nexus-prisma, graphql-codegen などの周辺のライブラリを利用して

「バックエンド -> graphql api -> フロントエンド まで完全にタイプセーフなコードを自動生成して爆速開発する環境」

を作ってみました。

ゴールは prisma スキーマを定義し、コマンド1つ投入するだけでコードが自動生成されるようにします。

前提事項

grqphql の知識がある方向けに書いています。

ソースコード

https://github.com/rk-tech0000/prisma-practice

prisma のバージョンについて

prisma は 1系と2系があり、1系は docker 上で動くことが前提のプロダクトになります。
2系はスタンドアローンに利用できるようですが、2019.11現在まだ preview 版です。
ここでは 1系を使います。

事前準備

prisma 実行クライアントの導入

まず prisma の各サービスを実行するための prisma cli を導入します。

$ npm install -g prisma
$ prisma -v
Prisma CLI version: prisma/1.34.8 (darwin-x64) node-v10.16.3

導入できました。

docker のインストール

prisma サーバを稼動させるため docker をイントールします。具体的な導入方法はここでは割愛します。

prisma ORM サーバ構築

公式の Get Started の記事を元に環境構築を行います。

docker-compose.yml の作成

prisma server と データベース(MySQL/PostgreSQL,MongoDB対応)環境を定義します。

以下は PostgreSQL の例です。

version: '3'
services:
  prisma:
    image: prismagraphql/prisma:1.34
    restart: always
    ports:
      - '4466:4466'
    environment:
      PRISMA_CONFIG: |
        port: 4466
        databases:
          default:
            connector: postgres
            host: postgres
            port: 5432
            user: prisma
            password: prisma
  postgres:
    image: postgres:10.3
    restart: always
    environment:
      POSTGRES_USER: prisma
      POSTGRES_PASSWORD: prisma
    volumes:
      - postgres:/var/lib/postgresql/data
volumes:
  postgres: ~

prisma server の起動

docker を起動します。

$ docker-compose up -d

http:localhost:4466 にアクセスすると prisma の playground 画面が見れるようになります。
簡単。api は未定義なので空っぽの状態です。

prisma cli で初期化を行います。

$ prisma init --endpoint http://localhost:4466
Created 2 new files:                                                                          

  prisma.yml           Prisma service definition
  datamodel.prisma    GraphQL SDL-based datamodel (foundation for database)

2ファイル作成されます。

  • prisma.yml
    prisma の設定ファイルです。後でここに設定を追加していきます。

  • datamodel.prisma
    prisma 独自のデータモデル定義ファイルになります。一番大事なやつです。
    中身を見ると User モデルが定義されています。

この状態ではまだデータベーススキーマは定義されていません。次のコマンドで行います。

$ prisma deploy
Creating stage default for service default ✔
Deploying service `default` to stage `default` to server `local` 575ms

Changes:

  User (Type)
  + Created type `User`
  + Created field `id` of type `ID!`
  + Created field `name` of type `String!`

Applying changes 1.1s

Your Prisma endpoint is live:

  HTTP:  http://localhost:4466
  WS:    ws://localhost:4466

You can view & edit your data here:

  Prisma Admin: http://localhost:4466/_admin

再度 http://localhost:4466 にアクセスします。 画面右端のタブの「DOCS」を開くと、graphql api が定義されているのが分かります。
おぉ、crud 処理が一通り出来上がっている!いい感じですね。

さらに prisma はもう一つcrud 操作が行える管理画面が用意されます。 http://localhost:4466/_admin 簡単な操作ならここで出来ます。

prisma client(ORM api) の作成

prisma は datamodel.prisma で定義された内容に従った ORM api の generate 機能を有しており prisma cli でコードを自動生成できます。
prisma init で生成された prisma.yml 設定ファイルに以下を追記します。

generate:
  - generator: typescript-client
    output: ./generated/prisma-client/

生成コマンドを実行します。

$ prisma generate
Generating schema 17ms
Saving Prisma Client (TypeScript) at /path/to/project/prisma-practice/generated/prisma-client/

output で指定した場所に2ファイル作成されます。

  • index.ts
    ORM api ファイルです。prisma client として生成されますが prisma はあくまで ORM なので、 これはブラウザで実行するものではなく、バックエンドサーバ上で実行する ORM api の位置付けのものです。

  • prisma-schema.ts
    graphql のスキーマ定義 dump です。

Graphql バックエンドサーバの構築

ここでエコシステムライブラリの nexus, nexus-prisma を利用して、バックエンドの graphql サーバ を超絶楽に実装していきます。

エコシステムライブラリ

https://nexus.js.org/
まず nexus の役割はソースコードでバックエンドサーバの graqphql タイプを定義するためのものです。 これを利用するとスキーマ定義だけでなく、合わせて TypeScript の型定義ファイルが自動生成されます。 nexus を単体で使う場合は、スキーマ定義のためのソースコードを自身で書く必要がありますが nexus-prisma ライブラリを利用することにより、その手間を軽減できます。

prisma の公式ページの nexus, nexus-prisma を使ったバックエンドサーバの構築手順を参考にやっていきます。

バックエンドサーバに必要なライブラリをインストールします。

npm init
npm install --save graphql-yoga nexus graphql nexus-prisma
npm install --save-dev ts-node-dev

nexus 向けのスキーマソースコードnexus-prisma-generateコマンドで自動生成します。
生成元になるデータは prisma generate で生成された./generated/prisma-clientです。
prisma.ymlには prisma deploy コマンドのhooksプロパティがあるので、生成コマンドを一連の処理にしていきます。

hooks:
  post-deploy:
    - prisma generate
    - npx nexus-prisma-generate --client ./generated/prisma-client --output ./generated/nexus-prisma

この hooks 設定を行うことで処理の流れは以下のようになります。
datamodel.prisma ->
(prisma deploy) ->
(prisma generate) ->
./generated/prisma-client ->
(nexus-prisma-generate) ->
./generated/nexus-prisma

やってみます。

$ prisma deploy
Deploying service `default` to stage `default` to server `local` 139ms
Service is already up to date.

post-deploy:

Generating schema... 14ms
Saving Prisma Client (TypeScript) at /path/to/project/prisma-practice/generated/prisma-client/

Running prisma generate ✔
Running npx nexus-prisma-generate --client ./generated/prisma-client --output ./generated/nexus-prisma...
Types generated at ./generated/nexus-prisma
Running npx nexus-prisma-generate --client ./generated/prisma-client --output ./generated/nexus-prisma ✔

生成されたコードを使ってバックエンドサーバのスキーマを設定します。schema.ts

import * as path from 'path'
import { prismaObjectType, makePrismaSchema } from 'nexus-prisma'
import datamodelInfo from './generated/nexus-prisma'
import { prisma } from './generated/prisma-client'

const Query = prismaObjectType({
  name: 'Query',
  definition (t) {
    t.prismaFields(['*'])
  },
})

const Mutation = prismaObjectType({
  name: 'Mutation',
  definition (t) {
    t.prismaFields(['*'])
  },
})

const schema = makePrismaSchema({
  types: [Query, Mutation],

  prisma: {
    datamodelInfo,
    client: prisma,
  },

  outputs: {
    schema: path.join(__dirname, './generated/schema.graphql'),
    typegen: path.join(__dirname, './generated/nexus.ts'),
  },
})

export default schema

ポイントはこの部分です。

t.prismaFields(['*'])

prisma deploy で生成した ORM api は全ての CRUD 処理が実装されています。
実際の grqphql サーバを作るときはユーザ権限等を考慮して公開する api を制限したりすると思いますが、この prismaFields の引数に 公開したいものだけを指定することでその制限を行う仕組みになっています。

上の例では*になっているので全て公開するという意味になります。

公開設定の具体例については、公式の以下を参考にしてください。 https://www.prisma.io/docs/1.28/get-started/03-build-graphql-servers-with-prisma-TYPESCRIPT-t201/#implement-graphql-api-based-on-crud-building-blocks

最後に graphql サーバの起動スクリプトを用意します。index.ts

import { GraphQLServer } from 'graphql-yoga'
import { prisma } from './generated/prisma-client'
import schema from './schema'

const server = new GraphQLServer({
  schema, // 1つ手前で作った schema.ts を食わせる
  context: { prisma }, // prisma orm api にアクセスするために context に追加
})
server.start(() => console.log('Server is running on http://localhost:4000'))

package.json に 開発サーバ起動スクリプト(dev)を追加し、サーバを起動します。

{
  "scripts": {
    ...
    "dev": "ts-node-dev --no-notify --respawn --transpileOnly ./index.ts"
  },
}
$ npm run dev
> prisma-practice@1.0.0 dev /path/to/project/prisma-practice
> ts-node-dev --no-notify --respawn --transpileOnly ./

Using ts-node version 7.0.1, typescript version 3.7.2
Server is running on http://localhost:4000

http://localhost:4000 にアクセスすると playground が表示され、 user crud が利用できるようになっているはずです。
ここまでで、ほぼソースコードを書かずに graphql api が構築できました。

Graphql クライアントの実装

残りはクライアント側です。graphql-codegenライブラリを使って クライアントコードを自動生成します。

ジェネレータの設定

https://graphql-code-generator.com/ https://graphql-code-generator.com/docs/getting-started/

$ npm install --save @graphql-codegen/cli
$ graphql-codegen init

対話形式で進めるとコード生成設定ファイル(codege.yml)が出力されます。

? What type of application are you building
今回は framework は利用しないので、Application built with other framework or vanilla JS を選択します。

? Where is your schema?
生成元になるスキーマファイルを選択します。
schema.ts内の outputs.schemaで指定されている./generated/schema.graphqlを指定します。

? Where are your operations and fragments?
graphql query ファイルを配置するディレクトリを指定します。client/gql/**/*.graphql

? Pick plugins: graphql-codegen 周辺のプラグインライブラリを一緒に導入します。
typescript 関連を選択します。

? Where to write the output:
生成先を指定します。 client/generated/graphql.ts

? Do you want to generate an introspection file?
Yes

? How to name the config file
デフォルトの codegen.ymlでOKです。

? What script in package.json should run the codegen?
コード生成を行う npm スクリプト名を指定します。ここではgql:genとします。

overwrite: true
schema: "generated/schema.graphql"
documents: "client/gql/**/*.graphql"
generates:
  client/generated/graphql.d.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-document-nodes
      - fragment-matcher

関連する plugin library が package.json に追加されるので、インストールします。

$ npm install

初期化時に設定したコード生成 script を実行します。

$ npm run gql:gen
> prisma-practice@1.0.0 gql:gen /path/to/project/prisma-practice
> graphql-codegen --config codegen.yml

  ✔ Parse configuration
  ✔ Generate outputs

サンプルクライアントコードの実装

apollo 関連ライブラリをインストールします

$ npm install apollo-boost graphql-tag node-fetch --save

生成したコードを利用するサンプルクライアントを実装します。client/sample.ts
(かなり雑です)

import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import ApolloClient from 'apollo-client'

import fetch from 'node-fetch';
import gql from 'graphql-tag'

import { User } from './generated/graphql'

const client = new ApolloClient({
  link: createHttpLink({
    uri: 'http://localhost:4000',
    fetch: fetch,
  }),
  cache: new InMemoryCache(),
})

client.query({
  query: gql`
    query {
      users {
        id
        name
      }
    }
  `,
})
.then(result => {
  const { users } : { users: User[] } = result.data

  users.forEach(user => {
    console.log('user.id:', user.id)
    console.log('user.name:', user.name)
  })
})
.catch(error => console.error(error));

生成された型情報を利用します。 型補完が効いていることが確認できると思います。

import { User } from './generated/graphql'

...
const { users } : { users: User[] } = result.data

users.forEach(user => {
console.log('user.id:', user.id)
console.log('user.name:', user.name)
})

ここまでの操作を一連の処理にするためクライアントの自動生成処理を prisma hooks に追加します。

hooks:
  post-deploy:
    - prisma generate
    - npx nexus-prisma-generate --client ./generated/prisma-client --output ./generated/nexus-prisma
    # 以下を追加
    - ts-node schema.ts
    - yarn gql:gen

これで準備が整いました。

prisma schema 駆動の味見

user モデルに 年齢(age)を追加することにします。

1.datamodel.prismaの User type に追加します。

type User {
  id: ID! @id
  name: String!
  # 追加
  age: Int
}

2.マイグレーションを実行します。

$ prisma deploy
Deploying service `default` to stage `default` to server `local` 148ms

Changes:

  User (Type)
  + Created field `age` of type `Int`

Applying changes 1.1s

post-deploy:

Generating schema... 13ms
Saving Prisma Client (TypeScript) at /path/to/project/prisma-practice/generated/prisma-client/

Running prisma generate ✔
npx: installed 91 in 4.959s

Running npx nexus-prisma-generate --client ./generated/prisma-client --output ./generated/nexus-prisma...
Types generated at ./generated/nexus-prisma
Running npx nexus-prisma-generate --client ./generated/prisma-client --output ./generated/nexus-prisma ✔
Running ts-node schema.ts ✔
yarn run v1.17.3
$ graphql-codegen --config codegen.yml
[22:04:19] Parse configuration [started]
[22:04:19] Parse configuration [completed]
[22:04:19] Generate outputs [started]
[22:04:19] Generate client/generated/graphql.d.ts [started]
[22:04:19] Generate ./graphql.schema.json [started]
[22:04:19] Load GraphQL schemas [started]
[22:04:19] Load GraphQL schemas [started]
[22:04:19] Load GraphQL schemas [completed]
[22:04:19] Load GraphQL schemas [completed]
[22:04:19] Load GraphQL documents [started]
[22:04:19] Load GraphQL documents [started]
[22:04:19] Load GraphQL documents [completed]
[22:04:19] Load GraphQL documents [completed]
[22:04:19] Generate [started]
[22:04:19] Generate [started]
[22:04:19] Generate [completed]
[22:04:19] Generate [completed]
[22:04:19] Generate client/generated/graphql.d.ts [completed]
[22:04:19] Generate ./graphql.schema.json [completed]
[22:04:19] Generate outputs [completed]
Done in 0.70s.

Running yarn gql:gen ✔
Warning: The `prisma generate` command was executed twice. Since Prisma 1.31, the Prisma client is generated automatically after running `prisma deploy`. It is not necessary to generate it via a `post-deploy` hook any more, you can therefore remove the hook if you do not need it otherwise.
Generating schema 17ms
Saving Prisma Client (TypeScript) at /path/to/project/prisma-practice/generated/prisma-client/

Your Prisma endpoint is live:

  HTTP:  http://localhost:4466
  WS:    ws://localhost:4466

You can view & edit your data here:

  Prisma Admin: http://localhost:4466/_admin

2オペで database, orm, graphql api, client api 全てに age に関するコードが自動で追加されます。

所感

これまで自前で graphql api をゴリゴリ書いていた自分としてはめちゃくちゃ楽なので気に入りました。
個人開発やちょっとしたものを作るのにはぴったりな気がします。

github の star 数は 16.1k(2019.11現在)でまだ少し足りず、日本の記事も少ないため微妙な存在と言えると思います。 また、prisma との比較対象に hasra というものがあるようです。
firebase との比較ですが、「graphql大好き」「nosql が馴染めない」「やっぱり型が欲しい」って感じの人には良いと思います。
ただし、prisma 1系は docker が前提なので、サーバレスでは作れないという点とマイグレーション機能が弱いなーという感じがします。
ここら辺は prisma 2系で改善されていくんでしょうか。

個人的には認証周りは firebase、ビジネスロジックprisma って感じで使おうと思っています。