🤖
Node.js / TypeScript / GraphQL で Web サービスを作ってきて、サーバーサイドはこの構成に落ち着きました
結論
下記の書き味が安定して良いと思いました。
- Knex/Kysely (Query Builder)
- Fastify (Web server)
- Nexus.js (GraphQL Schema Builder)
- 追記(2024-07): 新規では gqtx 使ってます
- BullMQ (Async job queue)
- ESBuild, esbuild-register (Transpiler)
- AVA (Test framework)
- PostgreSQL (Database)
基準は
- Boilerplate code が少ない
- 型安全に書ける
- マジックなコードが少ない。ダサくて良いから、辿れば分かるようにする
- テスト書きやすい
- ビルド・実行速度ともにパフォーマンス良い
です。
追記(2024-07): Bun も可能な範囲で使い始めたが、 Node を前提としたパッケージがまだまだ多くて本番運用は難しそう
今までに試したもの
- Jest
- デファクトスタンダードになっているが...
- ちゃんとやらないとテスト終了しなくて面倒
- global に定義されるのが個人的に好きではない
- Type-GraphQL
- デコレータ重すぎて挫折
- クラス強制がつらい。
- 具体的には、継承されたメソッドを override した場合、型推論が効かない。
- TypeDI/tsyringe
- Type-GraphQL と同じ。
- Node では DI するメリットないと思う
- ts-node
- 唯一 TypeScript のデコレーターを ( emitDecoratorMetadata, reflect-metadata 含め) 解釈できるが、 transpile-only でもめちゃ重い
- かといって SWC をトランスパイラに使うと、デコレーターを解釈できない。これは ESBuild も同じ
- 正確には、 Primitive な値は解釈できるが、デコレートされた
class
は解釈されなかった。
- 正確には、 Primitive な値は解釈できるが、デコレートされた
- Express
- 開発終了
- と思ったが4日前に新バージョンが出ていた
- https://github.com/expressjs/express/releases/tag/4.17.2
- Koa
- 速いけど薄い
- これはこれで良さそうな気がしている
- 確か起動は Fastify より速かったはず
- NestJS
- 良さげだけど、コードが多くなると Webpack でのビルドが遅いという噂によって断念
- Angular 使ってたら似てるので良いかも
- Objection.js
- 愛用していたが、 TypeScript との相性が悪い部分があり、離脱
- 具体的には、
where
句で型安全にならない、一部の処理で any になってしまうなど - 無理やり patch していたが、逆に分かりにくいなと
- 具体的には、
- ORM いらないという結論になった
- 愛用していたが、 TypeScript との相性が悪い部分があり、離脱
- TypeORM
- with が in じゃなくて Join でしかできないのがつらみ。
- デコレーター重い
- Mikro-orm
- デコレーター重い
-
where
がany
になる
- 素の GraphQL
- 型安全性が...
- graphql-code-generator でも Custom Model との Mapping の調整が面倒
- class 自体、ほぼ使わなくなった
- function だけで全く困らない
残ったもの
- Knex/Kysely
- 個人的に ORM は不要と思っているので、クエリビルダーくらいの抽象化で止めている
- Kysely の方が型安全でシンプルなので、新規プロジェクトではそっちを使っている
- Fastify
- 薄いのに意外と機能が多い
- そして速い
- Mcollina さんがアクティブに開発している
- 周辺ライブラリがめちゃめちゃ充実している
- esbuild
- デコレーターを諦めれば、いい感じに使える
- ts-node-transpile-only より圧倒的に速いので嬉しい
- esbuild-register もあって使いやすい
- 追記: 最近
tsx
等もあるけど、それよりなぜか速い。
- Nexus
- 文法や Codegen は好みがありそうだが、今んとこいい感じ
- Codegen には不満を持っている人も多そうだが、 TypeScript の型定義と戦うより、 Codegen の方が扱いやすいかな
- デコレーター使わないから、速い
- Entity と Schema を分離できるのは、やや二度手間だけど、実際やってみると気になならない
- 追記(2024-07): 新規では gqtx 使ってます。 Nexus は感覚的にやや遅いのと、エディタとの相性が gqtx の方が良いので
- AVA
- 常に素直に動いてくれる
- グローバル定義もないし、明示的に書ける
- ドキュメントも充実
- Tap や Tape でも 良いが...
- 追記(2024-07): Worker を使うようになって、ひと工夫しないと強制終了しなくなった
- BullMQ
- 非同期ジョブのデファクト
- Bull Board が良い
ディレクトリ構成はこんな感じ
src/
|-- app.ts
|-- async-jobs # 非同期ジョブ
| `-- onUserCreatedJob.ts
|-- bull-board.ts
|-- config # 環境変数を読み込む
| `-- index.ts
|-- entities # エンティティのドメインロジックを記述する
| |-- User.test.ts
| `-- User.ts
|-- graphql # GraphQLに関するもの
| |-- context.ts
| |-- __generated__ # Nexus が生成したファイル
| | |-- nexus-typegen.ts
| | `-- schema.graphql
| |-- schema
| | |-- UserTypeEnum.ts
| | |-- UserType.ts
| | |-- QueryType.ts
| | `-- MutationType.ts
| |-- schema.test
| | |-- helper.ts
| | |-- createUser.test.ts
| | `-- UserType.test.ts
| |-- schema.ts
| `-- shared # 共通して利用するロジックを記述
| `-- ensureAdmin.ts
|-- main.ts
|-- infrastructure # 外部サービスへの Gateway
| |-- db.ts
| |-- redis.ts
| `-- twilio.ts
|-- register.js # esbuild-register などを読み込むファイル
|-- repositories # DB からの取得ロジック
| `-- UserRepository.ts
|-- routes # REST のルーティング
| `-- UserRoute.ts
|-- use-cases # ビジネスロジックとなる一連操作を記述
| |-- findOrCreateUser.ts
| `-- someUseCase.ts
`-- utils # ユーティリティ
|-- asyncIterator.ts
`-- db.ts
コード例
ビジネスロジックの記述はこんな感じ
Entity
export interface User extends BaseModel {
firebaseUid: string;
email: string;
name: string;
isAdmin: boolean;
isTestUser: boolean;
}
export function isInternal(user: Pick<User, 'isAdmin' | 'isTestUser'>) {
return user.isAdmin || user.isTestUser;
}
Use Case
export CreateUserUseCaseArgs {
firebaseUid: string;
email: string;
name: string;
}
export async function createUserUseCase(props: CreateUserUseCaseArgs) {
// ...
}
Discussion