Slide 1

Slide 1 text

Fastify + Zod + Prisma さくさくTypeScriptバックエンド開発 2022/11/21 AWS事業本部コンサルティング部 ⾨別 優多

Slide 2

Slide 2 text

ハッシュタグ: #devio2022

Slide 3

Slide 3 text

イベント終了後 スライド & ソースコードを公開します!

Slide 4

Slide 4 text

4 ※TypeScriptでバックエンド開発をされた事が ある方を対象のスライドです (AWS要素は無いです) Fastify + Zod + Prisma + TypeScript 使っときゃ良い感じになるだけ 覚えて帰ってください!

Slide 5

Slide 5 text

5 スキーマファースト開発 してますか?(挨拶)

Slide 6

Slide 6 text

6 まさか手動でOpenAPI(Swagger) 書いて無いですよね・・・?

Slide 7

Slide 7 text

7 ・・・まさかExcelでAPI定義書なんて 書いて無いですよね・・・?

Slide 8

Slide 8 text

8 今日はなすこと 1. スキーマファースト開発について 2. Fastify + JSON Schema + Zod を使った構成 3. Prismaはいいぞ

Slide 9

Slide 9 text

9 自己紹介 門別 優多 – moko クラスメソッド株式会社 AWS事業本部コンサルティング部 入社: 2019/07 Twitter, GitHub: @mokocm 2019年7月〜2021年6月 コンサルティング部 2021年7月〜2022年6月 CX事業本部MAD事業部 2022年7月〜コンサルティング部 2020-2022 APN AWS Top Engineer 2021 APN ALL AWS Certifications Engineer 2022年技能五輪国際大会クラウドコンピューティング職種 優秀賞 好きなAWSサービス: AWS CDK YAMLメンテ職人(もうやりたくない)

Slide 10

Slide 10 text

10 元ネタ https://dev.classmethod.jp/articles/fastify-zod-openapi/

Slide 11

Slide 11 text

11 スキーマファースト開発?

Slide 12

Slide 12 text

12 スキーマファースト開発 • スキーマを起点とした開発手法 • OpenAPI(Swagger)を定義して、スキーマを元にコードを生成して開 発を進める • OpenAPI(Swagger)で定義したスキーマを各チームで合意 • フロント, 各マイクロサービス間等で実装待ちの時間を省ける • Input/Outputが明記されるため、実装もしやすい

Slide 13

Slide 13 text

13 OpenAPI(Swagger) • API設計をYAML, JSON等で記述 • Git管理することによりバージョン管理 & Pull Requestでレビュー できる • WebUIでAPIの確認、実行が可能 • OpenAPI(Swagger)から言語の壁を越えて型を作成可能 • バックエンド、フロントと別ける場合などとても便利

Slide 14

Slide 14 text

14 OpenAPI(Swagger)を手動で書く • OpenAPI(Swagger)は下記のような記述で可能

Slide 15

Slide 15 text

15 OpenAPI(Swagger)を手動で書く

Slide 16

Slide 16 text

16 OpenAPI(Swagger)を手動で書く→課題がたくさん • 1万行を超えるOpenAPIファイルの爆誕 • JetBrainsのBookmark機能に救われました… • 宣言的に何度も同じような内容を書く必要のある箇所が多すぎて 苦しい • ファイル分割するも可読性に難あり • そもそもEditorによってはファイル分割にPluginが対応していない 場合もある ↑とあるプロジェクトで YAML 1万行をメンテした時に言われたチャット

Slide 17

Slide 17 text

17 つらい • 不幸な事にOpenAPIから型を生成していなかった場合 • OpenAPIとバックエンドで利用する型を二箇所で手動記述 • OpenAPIとバックエンドの実装で乖離が発生・・・ • OpenAPIで定義したバリデーションと実装の乖離 • 1万行のYAMLとバックエンドで利用する型との差分をどうやって探 す・・・ ↑1万行のSwaggerに殺される人 ※1pt=約1.5h程度

Slide 18

Slide 18 text

解決方法は2つ

Slide 19

Slide 19 text

(1) OpenAPIファイルからプログラミング言語の 型を作成 型からValidation, Serialization, 実装で利用する ための型ファイルを出力

Slide 20

Slide 20 text

20 素晴らしいライブラリがたくさん https://www.npmjs.com/package/swagger-typescript-api

Slide 21

Slide 21 text

クライアント側などでは重宝するが、 根本的にYAMLを手動で書いているようじゃ 1万行YAMLメンテ職人から脱せない

Slide 22

Slide 22 text

22 課題を整理 1. 手動でOpenAPI(Swagger)を書きたくない 2. OpenAPI(Swagger)を自動出力したい 3. バックエンド側の型定義とかValidation, Serializationとか良い感じ にやりたい 4. リクエスト・レスポンススキーマの重複管理したくない 5. もう二度と1万行のYAMLメンテしたくない ↑無駄技術を習得してしまった図

Slide 23

Slide 23 text

これらを解決する方法、あります。

Slide 24

Slide 24 text

(2) OpenAPIを何らかのライブラリを利用して プログラム側から記述、出力

Slide 25

Slide 25 text

25 Fastify https://www.npmjs.com/package/fastify

Slide 26

Slide 26 text

• TypeScript(JavaScript) API Framework • TypeScriptでAPI作るならExpressに代わるファーストチョイスになってきた • Expressのつらい部分、遅い部分を解消 • 早くて使いやすい 26 Fastify import Fastify, { FastifyInstance, FastifyReply } from "fastify"; const server = Fastify({ logger: true, }); server.get("/", async (req: FastifyRequest, reply: FastifyReply) => { return { hello: "world" }; });

Slide 27

Slide 27 text

• FastifyでのValidationとSerializationはAjv JSON Schema Validatorで実行される • FastifyのRouteにJSON Schema形式でを定 義できる • ※厳密にはJSON Schema Draft 7 • Routeに入れたJSON Schemaを元に @fastify/swagger @fastify/swagger-uiを使 えばOpenAPI(Swagger)出力可能 • ちなみにOpenAPIはJSON Schema Draft 00 に準拠している • =ほぼ互換がある • JSON Schema手動で書くのはしんどい • 実質OpenAPI(Swagger)書いているようなもん 27 FastifyでのValidation&SerializationとOpenAPI(Swagger)

Slide 28

Slide 28 text

28 Zod https://www.npmjs.com/package/zod

Slide 29

Slide 29 text

• TypeScript界最強の静的型付けバリデーション • z.number().min(0).max(100).nullable()のようにチェーンで書いてい ける • TypeScriptで使うための型を出力可能 • Zodでスキーマ定義 〜バリデーションを行うのが一般的 29 Zod import { z } from "zod"; const Blog = z.object({ blog_id: z.number(), title: z.string().max(50), description: z.string().max(200), body: z.string().describe("ブログの中身(Markdown)"), thumnail_url: z.string().url().optional().describe("サムネイル(optional)"), author_id: z.string().describe("投稿者"), created_at: z.date().describe("作成日"), });

Slide 30

Slide 30 text

30 zod-to-json-schema https://www.npmjs.com/package/zod-to-json-schema

Slide 31

Slide 31 text

31 zod-to-json-schema • ZodからJSON Schemaを出力可能 • Zodの書きやすさ、Fastifyでのバリデーション、OpenAPI(Swagger) 出力、良いところ取り!! // TypeScriptの型 export type GetBlogsOutput = z.infer; // JSON Schema export const GetBlogsOutput = zodToJsonSchema(Blog, { name: "blog", }).definitions.blog;

Slide 32

Slide 32 text

32 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer でZodObjectを食わしてTypeScriptの型を作成 バックエンドでの実装の際に利用 3. zot-to-json-schema で JSON Schemaを作成 4. FastifyのJSON Schemaに食わせる 5. AjvがJSON Schemaを元に良い感じにRequest/Responseをチェックしてくれる 6. ついでに @fastify/swagger が Swagger (OpenAPI v2) または OpenAPI v3 を作ってくれる 7. それらを @fastify/swagger-ui でブラウザで表示する事が可能 JSON Schemaを中間言語とすることでZod, Fastify, Ajv, OpenAPI (Swagger) 間の壁を良い感じに緩衝してくれて便利だね!

Slide 33

Slide 33 text

33 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer でZodObjectを食わしてTypeScriptの型を作成 バックエンドでの実装の際に利用 3. zot-to-json-schema で JSON Schemaを作成 4. FastifyのJSON Schemaに食わせる 5. AjvがJSON Schemaを元に良い感じにRequest/Responseをチェックしてくれる 6. ついでに @fastify/swagger が Swagger (OpenAPI v2) または OpenAPI v3 を作ってくれる 7. それらを @fastify/swagger-ui でブラウザで表示する事が可能 JSON Schemaを中間言語とすることでZod, Fastify, Ajv, OpenAPI (Swagger) 間の壁を良い感じに緩衝してくれて便利だね! Zod一度定義しとけばmonorepo構成でフロント側でもZodの機能を使ったvalidationをできたりtRPC 導入してEnd to End typesafeできたりするよ!便利だね! ちなみに最近の @fastify/swaggerのアップデートで@fastify/swaggerにSwaggerをWebで表示する機能 が @fastify/swagger-ui に分離される破壊的変更があったよ!

Slide 34

Slide 34 text

34 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer でZodObjectを食わしてTypeScriptの型を作成 バックエンドでの実装の際に利用 3. zot-to-json-schema で JSON Schemaを作成 4. FastifyのJSON Schemaに食わせる 5. AjvがJSON Schemaを元に良い感じにRequest/Responseをチェックしてくれる 6. ついでに @fastify/swagger が Swagger (OpenAPI v2) または OpenAPI v3 を作ってくれる 7. それらを @fastify/swagger-ui でブラウザで表示する事が可能 JSON Schemaを中間言語とすることでZod, Fastify, Ajv, OpenAPI (Swagger) 間の壁を良い感じに緩衝してくれて便利だね! Zod一度定義しとけばmonorepo構成でフロント側でもZodの機能を使ったvalidationをできたりtRPC 導入してEnd to End typesafeできたりするよ!便利だね! ちなみに最近の @fastify/swaggerのアップデートで@fastify/swaggerにSwaggerをWebで表示する機能 が @fastify/swagger-ui に分離される破壊的変更があったよ! Fastifyは活発にアップデートされていくので日々のウォッチが必要だね!そういえばzod-to-json-schema はconst assertionをz.nativeEnum()に入れた物を食わせると空配列が帰ってくるバグがあったけど自分で 直してPRを送って先日Mergeされたよ!まだZodとFastifyを組み合わせて使ってる人が少ないので 今回の登壇を元にしてチャレンジしてくれると嬉しいな!めちゃめちゃ便利だよ!

Slide 35

Slide 35 text

35 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer でZodObjectを食わしてTypeScriptの型を作成 バックエンドでの実装の際に利用 3. zot-to-json-schema で JSON Schemaを作成 4. FastifyのJSON Schemaに食わせる 5. AjvがJSON Schemaを元に良い感じにRequest/Responseをチェックしてくれる 6. ついでに @fastify/swagger が Swagger (OpenAPI v2) または OpenAPI v3 を作ってくれる 7. それらを @fastify/swagger-ui でブラウザで表示する事が可能 JSON Schemaを中間言語とすることでZod, Fastify, Ajv, OpenAPI (Swagger) 間の壁を良い感じに緩衝してくれて便利だね! Zod一度定義しとけばmonorepo構成でフロント側でもZodの機能を使ったvalidationをできたりtRPC 導入してEnd to End typesafeできたりするよ!便利だね! ちなみに最近の @fastify/swaggerのアップデートで@fastify/swaggerにSwaggerをWebで表示する機能 が @fastify/swagger-ui に分離される破壊的変更があったよ! Fastifyは活発にアップデートされていくので日々のウォッチが必要だね!そういえばzod-to-json-schema はconst assertionをz.nativeEnum()に入れた物を食わせると空配列が帰ってくるバグがあったけど自分で 直してPRを送って先日Mergeされたよ!まだZodとFastifyを組み合わせて使ってる人が少ないので 今回の登壇を元にしてチャレンジしてくれると嬉しいな!めちゃめちゃ便利だよ! わからん ※ここまで理解するまで半月かかった

Slide 36

Slide 36 text

ようするに

Slide 37

Slide 37 text

37 ようするに 1. Zodのバリデーションコードを書く 3. 下処理したFastifyに食わせる 2. JSON SchemaとTypeScript向けの型を作る import { z } from "zod"; const Blog = z.object({ blog_id: z.number(), title: z.string().max(50), description: z.string().max(200), body: z.string().describe("ブログの中身(Markdown)"), thumnail_url: z.string().url().optional().describe("サムネイル(optional)"), author: z.string().describe("投稿者"), created_at: z.date().describe("作成日"), }); const getBlogsOutput = z.array(Blog) // TypeScriptの型 export type GetBlogsOutput = z.infer; // JSON Schema export const GetBlogsOutput = zodToJsonSchema(getBlogsOutput, { name: ”getBlogOutput", }).definitions.getBlogOutput; server.get("/blogs", { schema: { response: { 200: { ...GetBlogsOutput, description: "取得成功” }, }, tags: ["blog"], }, handler: getBlogsHandler, });

Slide 38

Slide 38 text

38 Reply

Slide 39

Slide 39 text

39 OpenAPI(Swagger)

Slide 40

Slide 40 text

40 課題を整理 ✅手動でOpenAPI(Swagger)を書きたくない ✅OpenAPI(Swagger)を自動出力したい ✅バックエンド側の型定義とかValidation, Serializationとか良い感じに やりたい ✅リクエスト・レスポンススキーマの重複管理したくない ✅✅✅もう二度と1万行のYAMLメンテしたくない 元ネタとなった記事を書いてくれたきんじょーさん、ありがとうございます。

Slide 41

Slide 41 text

ちなみに

Slide 42

Slide 42 text

42 json-schema-to-zod 既存のJSON SchemaからZodに変換する神ライブラリも存在します https://github.com/StefanTerdell/json-schema-to-zod

Slide 43

Slide 43 text

本日の1万行のYAMLを倒す会は以上です。 ご静聴ありがとうございました・・・?

Slide 44

Slide 44 text

Fastify + Zod + Prisma さくさくTypeScriptバックエンド開発 2022/11/21 AWS事業本部コンサルティング部 ⾨別 優多

Slide 45

Slide 45 text

45 いつの間にか1万行YAMLに翻弄されて つらい話かしてなかった

Slide 46

Slide 46 text

46 時間ないので

Slide 47

Slide 47 text

47 結論 新規プロジェクトで TypeScript + RDBMS使うなら Prisma 使ってりゃ なんとかなる

Slide 48

Slide 48 text

48 Prisma Next-generation ORM for Node.js & TypeScript TypeScriptでORM使うなら TypeORM と Prismaどちらかが採用されがち TypeORMより癖が無く、型もきちんとしていて書きやすいので、 黙ってPrisma使っときゃ良いのでは(個人の感想)

Slide 49

Slide 49 text

49 TypeORMのつらみ • Databaseを定義するEntityで、「データベース向け」の型と、 「TypeScript向けの型」を定義する必要があり、型の嘘がつけてし まう • nullable: true なカラムでTypeScriptではNullを許容しないケースなど export class Blog { @PrimaryGeneratedColumn({ name: ‘blog_id’, type: 'int’, }) blog_id: number @Column({ name: 'thumbnail_url’, type: 'varchar’, default: null, nullable: true, }) thumbnail_url: string // | null }

Slide 50

Slide 50 text

50 TypeORMのつらみ • QueryBuilderを使って if 文の中で .andWhere()で条件を書ける • とても便利だが、可読性が悪くなりがち • ※500行近いleftJoin, andWhere地獄を見た感想 const output = getManager().getRepository(Blog).createQueryBuilder('blog'); if (condition) { output.andWhere(...query); } else if { output.leftJoinAndSelect(...leftJoin) output.andWhere(...query); } else { output.andWhere(...otherQuery) }

Slide 51

Slide 51 text

51 PrismaでのSchema定義

Slide 52

Slide 52 text

52 Write const blog = await prisma.blog.create({ data: { title: body.title, description: body.description, body: body.body, thumnailUrl: body.thumnail_url, authorId: body.author_id, }, });

Slide 53

Slide 53 text

53 Read const blogs = await prisma.blog.findMany(); const blogs = await prisma.blog.findMany({ where: { blogId: 1, }, }); const blogs = await prisma.blog.findMany({ where: { blogId: body.blog_id ?? undefined }, include: { authorUser: true, }, }); FindMany where where and join ・Where内のvalueがundefinedの場合、Prismaは「何もしない」。三項演算子、null合体演算子などで簡単に振る舞いを変えられる ・超複雑な条件がある場合もだいたいfindMany内だけで簡潔できる ・全て(SQL直書き以外)において型が効くため、安全に利用できる。最高! ・既存DBからのdb pull、改修後のMigration実施なども可能

Slide 54

Slide 54 text

54 まとめ

Slide 55

Slide 55 text

Fastify + Zod + Prisma、本当に便利なので みんなもっと使おう!!!!!!!

Slide 56

Slide 56 text

No content