Share
Embed
Transcript
56 slides
-
4 ※TypeScriptでバックエンド開発をされた事が ある方を対象のスライドです (AWS要素は無いです) Fastify + Zod + Prisma +
TypeScript 使っときゃ良い感じになるだけ 覚えて帰ってください! -
8 今日はなすこと 1. スキーマファースト開発について 2. Fastify + JSON Schema +
Zod を使った構成 3. Prismaはいいぞ -
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メンテ職人(もうやりたくない) -
12 スキーマファースト開発 • スキーマを起点とした開発手法 • OpenAPI(Swagger)を定義して、スキーマを元にコードを生成して開 発を進める • OpenAPI(Swagger)で定義したスキーマを各チームで合意 •
フロント, 各マイクロサービス間等で実装待ちの時間を省ける • Input/Outputが明記されるため、実装もしやすい -
13 OpenAPI(Swagger) • API設計をYAML, JSON等で記述 • Git管理することによりバージョン管理 & Pull Requestでレビュー
できる • WebUIでAPIの確認、実行が可能 • OpenAPI(Swagger)から言語の壁を越えて型を作成可能 • バックエンド、フロントと別ける場合などとても便利 -
16 OpenAPI(Swagger)を手動で書く→課題がたくさん • 1万行を超えるOpenAPIファイルの爆誕 • JetBrainsのBookmark機能に救われました… • 宣言的に何度も同じような内容を書く必要のある箇所が多すぎて 苦しい •
ファイル分割するも可読性に難あり • そもそもEditorによってはファイル分割にPluginが対応していない 場合もある ↑とあるプロジェクトで YAML 1万行をメンテした時に言われたチャット -
17 つらい • 不幸な事にOpenAPIから型を生成していなかった場合 • OpenAPIとバックエンドで利用する型を二箇所で手動記述 • OpenAPIとバックエンドの実装で乖離が発生・・・ • OpenAPIで定義したバリデーションと実装の乖離
• 1万行のYAMLとバックエンドで利用する型との差分をどうやって探 す・・・ ↑1万行のSwaggerに殺される人 ※1pt=約1.5h程度 -
22 課題を整理 1. 手動でOpenAPI(Swagger)を書きたくない 2. OpenAPI(Swagger)を自動出力したい 3. バックエンド側の型定義とかValidation, Serializationとか良い感じ にやりたい
4. リクエスト・レスポンススキーマの重複管理したくない 5. もう二度と1万行のYAMLメンテしたくない ↑無駄技術を習得してしまった図 -
• 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" }; }); -
• 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) -
• 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("作成日"), }); -
31 zod-to-json-schema • ZodからJSON Schemaを出力可能 • Zodの書きやすさ、Fastifyでのバリデーション、OpenAPI(Swagger) 出力、良いところ取り!! // TypeScriptの型
export type GetBlogsOutput = z.infer<typeof Blog>; // JSON Schema export const GetBlogsOutput = zodToJsonSchema(Blog, { name: "blog", }).definitions.blog; -
32 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer<typeof > で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) 間の壁を良い感じに緩衝してくれて便利だね! -
33 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer<typeof > で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 に分離される破壊的変更があったよ! -
34 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer<typeof > で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を組み合わせて使ってる人が少ないので 今回の登壇を元にしてチャレンジしてくれると嬉しいな!めちゃめちゃ便利だよ! -
35 まとめると 1. Zodスキーマを作成(Zodで定義)。なお今回はZodではバリデーションをしない 2. z.infer<typeof > で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を組み合わせて使ってる人が少ないので 今回の登壇を元にしてチャレンジしてくれると嬉しいな!めちゃめちゃ便利だよ! わからん ※ここまで理解するまで半月かかった -
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<typeof getBlogsOutput >; // JSON Schema export const GetBlogsOutput = zodToJsonSchema(getBlogsOutput, { name: ”getBlogOutput", }).definitions.getBlogOutput; server.get("/blogs", { schema: { response: { 200: { ...GetBlogsOutput, description: "取得成功” }, }, tags: ["blog"], }, handler: getBlogsHandler, }); -
48 Prisma Next-generation ORM for Node.js & TypeScript TypeScriptでORM使うなら TypeORM
と Prismaどちらかが採用されがち TypeORMより癖が無く、型もきちんとしていて書きやすいので、 黙ってPrisma使っときゃ良いのでは(個人の感想) -
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 } -
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) } -
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, }, }); -
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実施なども可能