Slide 1

Slide 1 text

2026.05.22 TSKaigi 2026 / Ryutaro Yako Copyright ©Unique Vision Company. All Rights Reserved. Zod v4 Codec で スキーマに型変換を埋め込む REST API 設計

Slide 2

Slide 2 text

Profile 名前: Ryutaro Yako 所属: ユニークビジョン株式会社 役割: テックリード Experience 7 名のエンジニアが所属する開発チーム toB 向けアプリケーションをフルスタック TypeScript で開発中 Copyright ©Unique Vision Company. All Rights Reserved. 自己紹介

Slide 3

Slide 3 text

社員約80名のうち、 約半数がエンジニアのテクノロジー企業 Engineers Others Copyright ©Unique Vision Company. All Rights Reserved. 会社概要

Slide 4

Slide 4 text

当社はソーシャルメディアを利用した、ソーシャルメディア総合管理ツールや X・LINE・Instagram・TikTokなどSNSマーケティングツールを多数提供しています。 Belugaスタジオ X/Facebook/Instagramなどのアカウ ントを一元管理できるソーシャルメディ ア総合管理ツールです。企業のSNS運用 をサポートします。 Beluga キャンペーン フォロー&リポストに代表されるXのイ ンスタントウィンをはじめとしたキャン ペーンを複数展開しています。質の高い キャンペーンの実施を支援しています。 その他 SNSキャンペーンに連動した、生成AIを 活用したシステムやプラットフォームが 提供する独自ツールやシステム開発も順 次行っています。 Copyright ©Unique Vision Company. All Rights Reserved. 事業内容

Slide 5

Slide 5 text

01. 背景と課題 02. Zod v4 Codec とは 03. Zod スキーマの設計 04. バックエンドでの利用方法 05. フロントエンドでの利用方法 06. まとめ Copyright ©Unique Vision Company. All Rights Reserved. アジェンダ

Slide 6

Slide 6 text

Copyright ©Unique Vision Company. All Rights Reserved. 背景と課題

Slide 7

Slide 7 text

フルスタック TypeScript の開発プロジェクト API サーバは REST API として実装 社内の他のプロジェクトでは Rust で API を開 発しており、設計を REST API に統一 tRPC などの利用は見送ることになった 技術スタック アプリ 技術スタック API サーバ Hono / @hono/zod-openapi フロントエンド Vue.js / hono/client Copyright ©Unique Vision Company. All Rights Reserved. 背景

Slide 8

Slide 8 text

モノレポを生かしてスキーマを共有している 型定義 バリデーションロジック しかし日時の表現が各層で異なり、変換が必要 アプリケーション: Dayjs オブジェクト API 通信: ISO 文字列 日時の変換を毎回手動で行うのは大変 変換ロジックもスキーマに集約したい apps/ ├── api/ # バックエンド └── client/ # フロントエンド packages/ └── schema/ # 共有スキーマ 共有スキーマ 共有 共有 バックエンド API 通信 フロントエンド 共有スキーマ 共有 共有 バックエンド Dayjs API 通信 string(ISO 文字列) フロントエンド Dayjs Copyright ©Unique Vision Company. All Rights Reserved. 課題

Slide 9

Slide 9 text

日時の変換を毎回手動で行うのは大変 const startAt = dayjs(body.startAt); // ← 手動で string → Dayjs に変換 const endAt = dayjs(body.endAt); startAt: inserted.startAt.toISOString(), // ← 手動で Dayjs → string に変換 endAt: inserted.endAt.toISOString(), app.openapi(postCampaignRoute, async (c) => { const body = c.req.valid("json"); const [inserted] = await db .insert(campaigns) .values({ name: body.name, startAt, endAt }) .returning(); return c.json( response.detail({ uuid: inserted.uuid, name: inserted.name, }), ); }); Copyright ©Unique Vision Company. All Rights Reserved. 課題 — 日時の変換を楽にしたい

Slide 10

Slide 10 text

Copyright ©Unique Vision Company. All Rights Reserved. Zod v4 Codec とは

Slide 11

Slide 11 text

Zod v4.1 から導入された機能 Zod のスキーマでデータを双方向変換できる 従来の preprocess / transform は単方向の 変換しかできない transform ⼊⼒の型 単⽅向のみ 出⼒の型 Codec ⼊⼒の型 Dayjs encode decode 出⼒の型 string(ISO 文字列) Copyright ©Unique Vision Company. All Rights Reserved. Zod v4 Codec とは

Slide 12

Slide 12 text

Zod スキーマ (Codec) 共有 共有 バックエンド Dayjs encode decode API 通信 string(ISO 文字列) decode encode フロントエンド Dayjs Copyright ©Unique Vision Company. All Rights Reserved. Codec で実現するデータフロー

Slide 13

Slide 13 text

日時を双方向変換するスキーマ 変換ロジックをスキーマに集約できる 利用例 変換ロジックを意識せずに使える export const datetimeSchema = () => z.codec( z.iso.datetime(), z.custom((val) => dayjs.isDayjs(val)), { decode: (val) => dayjs(val), encode: (val) => val?.toISOString(), }, ); export const campaignSchema = z.object({ uuid: z.uuid(), name: z.string(), startAt: datetimeSchema(), endAt: datetimeSchema(), }); export const campaignCreateSchema = campaignSchema.omit({ uuid: true }); Copyright ©Unique Vision Company. All Rights Reserved. Zod スキーマの設計

Slide 14

Slide 14 text

Copyright ©Unique Vision Company. All Rights Reserved. バックエンドでの利用方法

Slide 15

Slide 15 text

@hono/zod-openapi でルートを定義する リクエスト・レスポンス両方に Zod スキーマを指定する body: { content: { "application/json": { schema: campaignCreateSchema } } }, "application/json": { schema: detailResponse(campaignSchema) }, export const postCampaignRoute = createRoute({ method: "post", path: "/api/v1/campaigns", request: { }, responses: { 200: { description: "キャンペーン", content: { }, }, }, }); Copyright ©Unique Vision Company. All Rights Reserved. バックエンドでの利用方法

Slide 16

Slide 16 text

ハンドラー実装例 — Codec 導入によりボイラープレート削減 @hono/zod-openapi がリクエストのバリデーションと日時の変換をする @hono/zod-openapi はレスポンスに対しては何もしない ユーティリティ response.detail を作成して encode 呼び出しを共通化 encode はスキーマに未定義のプロパティを除去する( inserted をそのまま渡せる) app.openapi(postCampaignRoute, async (c) => { const body = c.req.valid("json"); const [inserted] = await c.var.db .insert(campaigns) .values({ name: body.name, startAt: body.startAt, endAt: body.endAt }) .returning(); return c.json(response.detail(campaignSchema, inserted)); }); Copyright ©Unique Vision Company. All Rights Reserved. バックエンドでの利用方法

Slide 17

Slide 17 text

Copyright ©Unique Vision Company. All Rights Reserved. フロントエンドでの利用方法

Slide 18

Slide 18 text

API クライアントを作成 hc ( hono/client )を利用する encode / decode をおこなう薄いラッパーを作成 export class Api { client = hc("").api.v1; async postCampaign(data: CampaignCreate): Promise { return requestData( this.client.campaigns.$post, { json: data }, { request: campaignCreateSchema, // encode: Dayjs → string response: campaignSchema, // decode: string → Dayjs }, ); } } Copyright ©Unique Vision Company. All Rights Reserved. フロントエンドでの利用方法

Slide 19

Slide 19 text

フォームでバリデーションするときの注意点 — parse ではなく encode を使う フォームの値は Dayjs オブジェクト parse 方向は string を期待するためエラーにな る encode 方向でバリデーションする Vue.js のコンポーザブルで共通化 Dayjs encode parse(decode 方向) string (ISO ⽂字列) const validate = ( schema: T, data: Partial>, ): z.output => { schema.encode(data as z.output); return data as z.output; }; Copyright ©Unique Vision Company. All Rights Reserved. フロントエンドでの利用方法

Slide 20

Slide 20 text

Copyright ©Unique Vision Company. All Rights Reserved. まとめ

Slide 21

Slide 21 text

Zod v4 Codec を使うと、スキーマに型変換ロジックを埋め込める ハンドラーや API クライアントで変換を意識せずに使える Hono・日時変換を題材にしたが、他のフレームワークやデータ型にも応用できる Zod スキーマ (Codec) 共有 共有 バックエンド Dayjs encode decode API 通信 string(ISO 文字列) decode encode フロントエンド Dayjs Copyright ©Unique Vision Company. All Rights Reserved. まとめ

Slide 22

Slide 22 text

Copyright ©Unique Vision Company. All Rights Reserved. ご清聴ありがとうございました