LoginSignup
20
6
この記事誰得? 私しか得しないニッチな技術で記事投稿!

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

Last updated at Posted at 2023-07-05

jslogos.png
「犬って1000種類くらいおんねん」ってタイトルにしようか悩みました。
こんにちは、モロ(@moro_is)です。

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

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

技術スタック

  • NestJS (Fastify)
    • なういAPIを作るやつ
    • FastifyモードだとExpressモードの倍くらい速いらしい
  • Prisma
    • ORM(ActiveRecordみたいなやつ?)
    • NestJS標準(?)のTypeORMに対して後発
  • Mercurius
    • なういGraphQL作るやつ
    • Apolloの後発
      • 多分太陽神アポロに対して水星の神メルクリウスをぶつけたのだと思う熱い(オタク特有の早口)
  • Supabase
    • Firebase代替のBaaS。なうい
    • GUIもそこそこイケてるのでHeadlessCMS的にも使えそう
    • 実は単体でGraphQL API作れるらしい
  • Railway
    • Heroku代替っぽくありつつCIもいけるなういサーバー
  • 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"]
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のページを参考にmain.tsを修正します。

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の接続

SupabaseNew 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の設定

公式のドキュメントを参考にschema.prismaに構成を追加していきます。

今回はサンプルとして、

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

のように作っていきます(多分下図の感じ)。
er.png
DogColorが多対多、DogSizeが多対一、DogColorが中間テーブルです(多分)。

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の拡張もあります。

$ npx prisma generate

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

PrismaのSeedファイルを作る

今回は読み取り専用のAPIになる予定なので公式のドキュメントを参考にSeedファイルを作って初期データを入れます。
必要なパッケージを追加して、

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

package.jsonprismaを追加します。

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ファイルを追加。

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();
  });
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のドキュメントを参考にわちゃわちゃ作っていきます。

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

$ yarn add @nestjs/graphql @nestjs/mercurius graphql mercurius
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.tsapp.service.tsは使わなさそうなのでimportと合わせてファイルごと消してしまいます。

NestJSの編集

NestJSだと、

$ npx nest generate resource dog

上記のようなコマンドでいい感じにCRUDなファイルを一発生成できるそうなのですが、今回はRだけでいいので下記のようにServiceとResolverだけ生成しました。

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

まずはPrismaのServiceと、

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を作ります。

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[];
}
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);
  }
}

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にアクセスしてGUIからGraphQLを操作したりできます。

さいごに

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

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

20
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
6