雰囲気で作る最強の犬GraphQL API (NestJS+Fastify+Prisma+Supabase)

![jslogos.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76653/0901a45e-d021-ef46-e535-b85514c5dd96.png)
「犬って1000種類くらいおんねん」ってタイトルにしようか悩みました。
こんにちは、モロ（[@moro_is](https://twitter.com/moro_is)）です。

タイトルのとおり、APIはとてもよいものです。
本当はAPIじゃなくて図鑑的なWebアプリを作りたかったのである意味途中経過なのですが、一旦それらしいものができたので備忘録として残します。

:::note warn
なお、テーマに則ってニッチなのはNestJSでもGraphQLでもなく**犬APIのほう**です。
:::

# 技術スタック

- [NestJS](https://nestjs.com/) ([Fastify](https://docs.nestjs.com/techniques/performance))
  - なういAPIを作るやつ
  - FastifyモードだとExpressモードの倍くらい速いらしい
- [Prisma](https://docs.nestjs.com/recipes/prisma)
  - ORM(ActiveRecordみたいなやつ？)
  - NestJS標準(？)のTypeORMに対して後発
- [Mercurius](https://docs.nestjs.com/graphql/quick-start)
  - なういGraphQL作るやつ
  - Apolloの後発
    - 多分太陽神アポロに対して水星の神メルクリウスをぶつけたのだと思う熱い(オタク特有の早口)
- [Supabase](https://supabase.com/)
  - Firebase代替のBaaS。なうい
  - GUIもそこそこイケてるのでHeadlessCMS的にも使えそう
  - 実は単体でGraphQL API作れるらしい
- [Railway](https://railway.app/)
  - Heroku代替っぽくありつつCIもいけるなういサーバー
- Docker
  - おなじみのやつ

全体的に**何かなうくて使ってみたかったやつ**を詰め合わせています。

## Dockerで開発環境を作る

「普通にローカルでやればいいんじゃ？」という気もしつつ、何かあったとき本番と環境を揃えるのが楽になる気がするので雑にやっておきます。

```docker:Dockerfile
FROM node:18-alpine3.16
WORKDIR /api

RUN yarn global add @nestjs/cli
COPY package.json yarn.lock ./
RUN yarn install --prod --frozen-lockfile

COPY . .
CMD ["yarn", "start"]
```

```yml:docker-compose.yml
version: "3.8"

services:
  api:
    container_name: api
    build:
      context: .
      dockerfile: Dockerfile
    tty: true
    volumes:
      - type: bind
        source: .
        target: /api
    ports:
      - "3000:3000"
```

```
$ docker compose up -d
```

`@nestjs/cli`も入ったのでそのままNestJSで新規プロジェクトを作ります。


```
$ docker compose exec api nest new .
```

## Fastifyの導入

Fastifyを入れて、不要になったExpressの方を削除します。

```
$ yarn add @nestjs/platform-fastify
$ yarn remove @nestjs/platform-express
```

[NestJSのFastifyのページ](https://docs.nestjs.com/techniques/performance)を参考に`main.ts`を修正します。

```diff_typescript:main.ts
import { NestFactory } from '@nestjs/core';
+ import {
+   FastifyAdapter,
+   NestFastifyApplication,
+ } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

async function bootstrap() {
-   const app = await NestFactory.create(AppModule);
-   await app.listen(3000);
+   const app = await NestFactory.create<NestFastifyApplication>(
+     AppModule,
+     new FastifyAdapter(),
+   );
+   await app.listen(3000, '0.0.0.0');
}
bootstrap();
```

## Prismaの導入

まずはPrismaをインストールして、

```
$ yarn add prisma --dev
```

`npx prisma init`するといい感じに必要なファイル(`schema.prisma`とか`.env`とか)が生成されます。

## Supabaseの接続

[Supabase](https://supabase.com/)で`New project`を作ったら、`Project Settings > Database > Connection Pooling > Connection string`から下記のようなパスをコピーし、

```
postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres
```

`.env`に追加します。

```.env
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres"
```

## Prismaの設定

[公式のドキュメント](https://www.prisma.io/docs/concepts/components/prisma-schema)を参考に`schema.prisma`に構成を追加していきます。

今回はサンプルとして、

- Color(犬の毛色)
- Dog(犬種)
- Size(犬のサイズ)

のように作っていきます(多分下図の感じ)。
![er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76653/7dbeb387-1521-070c-3460-2e3642218e1e.png)
`Dog`と`Color`が多対多、`Dog`と`Size`が多対一、`DogColor`が中間テーブルです(多分)。

```js:prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Color {
  id          Int     @id @default(autoincrement())
  name        String  @unique
  description String?
  dog         Dog[]
}

model DogColor {
  dog     Dog   @relation(fields: [dogId], references: [id])
  dogId   Int
  color   Color @relation(fields: [colorId], references: [id])
  colorId Int

  @@id([dogId, colorId])
  @@unique([dogId, colorId])
}

model Dog {
  id          Int     @id @default(autoincrement())
  name        String  @unique
  description String?
  size        Size?   @relation(fields: [sizeId], references: [id])
  sizeId      Int?
  colors      Color[]
}

model Size {
  id           Int     @id @default(autoincrement())
  name         String  @unique
  descriptionJ String?
  dog          Dog[]
}
```

ちなみにいい感じに整形してくれる[VSCodeの拡張](https://marketplace.visualstudio.com/items?itemName=Prisma.prisma)もあります。

```
$ npx prisma generate
```

`schema.prisma`を触ったらとりあえず`prisma generate`。

## PrismaのSeedファイルを作る

今回は読み取り専用のAPIになる予定なので[公式のドキュメント](https://www.prisma.io/docs/guides/migrate/seed-database)を参考にSeedファイルを作って初期データを入れます。
必要なパッケージを追加して、

```
$ yarn add ts-node typescript @types/node --dev
```

`package.json`に`prisma`を追加します。

```diff_json:package.json
{
  "name": "my-project",
  "version": "1.0.0",
+ "prisma": {
+   "seed": "ts-node prisma/seed.ts"
+ },
  "devDependencies": {
    "@types/node": "^14.14.21",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.3"
  }
}
```

続いて各seedファイルを追加。

```typescript:prisma/seeds/seed.ts
import { PrismaClient } from '@prisma/client';
import seedColors from './color';
import seedDogColors from './dogColor';
import seedDogs from './dog';
import seedSizes from './size';

const prisma = new PrismaClient();

async function main() {
  await seedColors();
  await seedDogs();
  await seedSizes();
  await seedDogColors(); // 中間テーブルは最後
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });
```

```typescript:prisma/seeds/dog.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

const dogs = [
  {
    id: 1,
    name: '柴犬',
    description: '日本犬。かわいい。',
    size: 2,
  },
  {
    id: 2,
    name: 'ウェルシュ・コーギー・ペンブローク',
    description: '短足。かわいい←最強',
    size: 2,
  },
];

async function seedDogs() {
  for (const dog of dogs) {
    const newDog = await prisma.dog.create({
      data: dog,
    });
    console.log(
      `Created new dog: (ID: ${newDog.id}) ${newDog.name}`,
    );
  }
}

export default seedDogs;
```

他ファイルは割愛します。

```
$ npx prisma db seed
```

上記`prisma db seed`コマンドを実行すると、下記のようにずらっとデータが格納されます(サンプルだから少ないけど)。

```
Created new dog: ID 1, 柴犬
Created new dog: ID 2, ウェルシュ・コーギー・ペンブローク

🌱  The seed command has been executed.
```

今回まったく使わなかったのでよくわかっていないのですが、

```
$ yarn prisma studio
```

でGUIからDBを触ったりもできるそうです。

## Mercuriusの導入

DBがざっくりできたので次はGraphQL周りです。
今度は[NestJSのドキュメント](https://docs.nestjs.com/graphql/quick-start)を参考にわちゃわちゃ作っていきます。

まずは必要なパッケージのインストール。

```
$ yarn add @nestjs/graphql @nestjs/mercurius graphql mercurius
```

```diff_typescript: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 { MercuriusDriver, MercuriusDriverConfig } from '@nestjs/mercurius';

@Module({
- imports: [],
+ imports: [
+   GraphQLModule.forRoot<MercuriusDriverConfig>({
+     driver: MercuriusDriver,
+     graphiql: true,
+   }),
+ ],
- controllers: [AppController],
- providers: [AppService],
})
export class AppModule {}
```

`app.controller.ts`と`app.service.ts`は使わなさそうなのでimportと合わせてファイルごと消してしまいます。


## NestJSの編集

NestJSだと、

```
$ npx nest generate resource dog
```

[上記のようなコマンド](https://docs.nestjs.com/recipes/crud-generator)でいい感じにCRUDなファイルを一発生成できるそうなのですが、今回はRだけでいいので下記のようにServiceとResolverだけ生成しました。

```
$ npx nest generate service dog
```
```
$ npx nest generate resolver dog
```

まずはPrismaのServiceと、

```typescript:src/prisma/prisma.service.ts
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}
```

各モデルのEntity、Resolver、Serviceを作ります。

```typescript:src/dog/dog.entity.ts
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Size } from '../size/size.entity';
import { DogColor } from '../dogColor/dogColor.entity';

@ObjectType()
export class Breed {
  @Field(() => ID)
  id: number;

  @Field()
  name: string;

  @Field({ nullable: true })
  description: string;

  @Field(() => Size, { nullable: true })
  size?: Size;

  @Field(() => [DogColor], { nullable: true })
  colors?: DogColor[];
}
```

```typescript:src/dog/dog.resolver.ts
import { Resolver, Query, Args } from '@nestjs/graphql';
import { DogService } from './dog.service';
import { Dog } from './dog.entity';

@Resolver(() => Dog)
export class DogResolver {
  constructor(private readonly dogService: DogService) {}

  @Query(() => [Dog], { name: 'allDog' })
  findAll() {
    return this.dogService.findAll();
  }

  @Query(() => Dog, { name: 'dog' })
  findOne(@Args('id', { type: () => Number }) id: number) {
    return this.dogService.findOne(id);
  }
}

```

```typescript:src/dog/dog.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Dog } from './dog.entity';

@Injectable()
export class DogService {
  constructor(private prisma: PrismaService) {}

  findAll(): Promise<Dog[]> {
    return this.prisma.dog.findMany();
  }

  findOne(id: number): Promise<Dog | null> {
    return this.prisma.dog.findUnique({
      where: { id },
    });
  }
}

```

ちなみに、ServiceだのResolverだのいまいちまだよくわかっていません。
雰囲気だけでやっていくと、最終的に下記のような構成になります。

```
project/
  ├ prisma/
  │  ├ seeds/
  │  │  ├ color.ts
  │  │  ├ dog.ts
  │  │  ├ dogColor.ts
  │  │  ├ seed.ts
  │  │  └ size.ts
  │  └ schema.prisma
  ├ src/
  │  ├ color/
  │  │  ├ color.entity.ts
  │  │  ├ color.resolver.ts
  │  │  └ color.service.ts
  │  ├ dog/
  │  │  ├ dog.entity.ts
  │  │  ├ dog.resolver.ts
  │  │  └ dog.service.ts
  │  ├ dogColor/
  │  │  └ dogColor.entity.ts
  │  ├ prisma/
  │  │  └ prisma.service.ts
  │  ├ size/
  │  │  ├ size.entity.ts
  │  │  ├ size.resolver.ts
  │  │  └ size.service.ts
  │  ├ app.module.ts
  │  └ main.ts
  ├ .env
  ├ docker-compose.yml
  ├ Dockerfile
  └ package.json
```

何も考えずにこうなりましたが、きっともっとなうい構成があるのだろうなとは感じています。
誰か教えてください。

そして、満を持して`yarn start`したところ、下記のようなエラーが出ました。

```
$ yarn start
Error: Cannot find module '@mercuriusjs/gateway'
```

何もわからない。一切の雑念を振り払い`yarn add`します。

```
$ yarn add @mercuriusjs/gateway
```


```
$ yarn start
yarn run v1.22.19
$ nest start
[Nest] 10981  - 07/05/2023, 9:03:24 AM     LOG [NestFactory] Starting Nest application...
[Nest] 10981  - 07/05/2023, 9:03:24 AM     LOG [InstanceLoader] AppModule dependencies initialized +27ms
[Nest] 10981  - 07/05/2023, 9:03:24 AM     LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +0ms
[Nest] 10981  - 07/05/2023, 9:03:24 AM     LOG [InstanceLoader] GraphQLModule dependencies initialized +1ms
[Nest] 10981  - 07/05/2023, 9:03:25 AM     LOG [GraphQLModule] Mapped {/graphql, POST} route +81ms
[Nest] 10981  - 07/05/2023, 9:03:25 AM     LOG [NestApplication] Nest application successfully started +603ms
```

:tada:

無事に稼働したら、[localhost:3000/graphiql](localhost:3000/graphiql)にアクセスしてGUIからGraphQLを操作したりできます。

# さいごに

ぬるっと終わりましたがお気づきでしょうか。**ここまで一切Railwayに触れていないことに**。
大慌てでやってみたものの、N+1とかCORSとかテストとかパフォーマンスとかいろいろ手が回らず……。
もろもろ修正したらリポジトリもろとも公開したい気持ちです(ドメインもとった！)。

いつか……フロントも…………。