「犬って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で開発環境を作る
「普通にローカルでやればいいんじゃ?」という気もしつつ、何かあったとき本番と環境を揃えるのが楽になる気がするので雑にやっておきます。
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"]
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
を修正します。
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でNew project
を作ったら、Project Settings > Database > Connection Pooling > Connection string
から下記のようなパスをコピーし、
postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres
.env
に追加します。
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres"
Prismaの設定
公式のドキュメントを参考にschema.prisma
に構成を追加していきます。
今回はサンプルとして、
- Color(犬の毛色)
- Dog(犬種)
- Size(犬のサイズ)
のように作っていきます(多分下図の感じ)。
Dog
とColor
が多対多、Dog
とSize
が多対一、DogColor
が中間テーブルです(多分)。
// 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.json
にprisma
を追加します。
{
"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ファイルを追加。
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();
});
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
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
上記のようなコマンドでいい感じにCRUDなファイルを一発生成できるそうなのですが、今回はRだけでいいので下記のようにServiceとResolverだけ生成しました。
$ npx nest generate service dog
$ npx nest generate resolver dog
まずはPrismaのServiceと、
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を作ります。
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[];
}
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);
}
}
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
無事に稼働したら、localhost:3000/graphiqlにアクセスしてGUIからGraphQLを操作したりできます。
さいごに
ぬるっと終わりましたがお気づきでしょうか。ここまで一切Railwayに触れていないことに。
大慌てでやってみたものの、N+1とかCORSとかテストとかパフォーマンスとかいろいろ手が回らず……。
もろもろ修正したらリポジトリもろとも公開したい気持ちです(ドメインもとった!)。
いつか……フロントも…………。