TypeScriptとZodで堅牢なAPIクライアントを構築する
Emily Parker
Product Engineer · Leapcell

はじめに
ウェブ開発の複雑な世界では、APIとのやり取りは日常の基本です。リクエストを送信し、データを受信します。単純なことですよね?しかし、必ずしもそうではありません。型指定されていないAPIレスポンスの静かな危険性は、ランタイムエラー、予期しない動作、そしてフラストレーションのたまるデバッグセッションの連鎖につながる可能性があります。ユーザーオブジェクトの 'id' が数値ではなく文字列だったり、'data' フィールドが配列だったり null だったりする場合を想像してみてください。これらの不整合は、実稼働環境で顕在化するまで隠されていることが多く、アプリケーションへの信頼を損ない、開発を遅らせます。まさにここで、型安全の力が非常に価値のあるものになります。APIレスポンスの構造を積極的に定義し、検証することで、これらの問題を早期に発見し、バグを防ぎ、開発者体験を向上させることができます。この記事では、TypeScriptとZodの強力な組み合わせを使用して、回復力があり型安全なAPIリクエストクライアントを構築する方法をガイドし、APIのやり取りを潜在的な地雷原から、しっかりと守られた要塞へと変革します。
コアコンセプトの説明
実装に入る前に、活用していく主要なツールとコンセプトを明確に理解することから始めましょう。
- TypeScript: 静的な型定義を追加することでJavaScriptを拡張するオープンソース言語です。開発者はオブジェクト、関数、変数の形状を定義でき、優れたツール、早期のエラー検出、およびコードの可読性と保守性の向上を可能にします。TypeScriptでは、データがどのように見えるべきかを記述します。
- Zod: TypeScriptファーストのスキーマ宣言および検証ライブラリです。Zodでは、ランタイムで受信データを検証するために使用できるデータスキーマを定義できます。強力な推論機能で有名であり、Zodスキーマを定義すると、TypeScriptは対応する静的型を自動的に推論できます。これにより、ZodはTypeScriptの理想的なパートナーとなり、受信した信頼できないデータが期待される型に準拠していることを保証する堅牢なメカニズムを提供します。Zodをデータのボディガードと考えて、アプリケーションに「行儀の良い」データのみが入るようにしてください。
- APIクライアント: バックエンドAPIへのHTTPリクエストを行い、レスポンスを処理する責任を負うモジュールまたは関数のセットです。HTTPプロトコルの詳細を抽象化し、アプリケーションロジックが外部サービスとやり取りするためのよりクリーンなインターフェースを提供します。
型安全なAPIクライアントの作成
ここでの基本的な原則は、各APIレスポンス構造に対してZodスキーマを定義し、そのスキーマを使用してデータを受信した直後にデータを検証することです。TypeScriptは、Zodの推論を活用して、コンパイル時の型安全性を提供してくれます。
プロジェクトのセットアップ
まず、必要なパッケージがインストールされていることを確認しましょう。
npm install axios zod typescript npm install --save-dev @types/node # 環境によっては必要
ZodでAPIスキーマを定義する
ユーザーと投稿を管理するAPIを操作していると想像してみましょう。これらのエンティティのZodスキーマを定義します。
// src/schemas.ts import { z } from 'zod'; // 単一ユーザーのスキーマ export const UserSchema = z.object({ id: z.number().int().positive(), name: z.string().min(1, 'Name cannot be empty'), email: z.string().email('Invalid email address'), age: z.number().int().positive().optional(), // オプションフィールド }); // ZodスキーマからTypeScript型を推論する export type User = z.infer<typeof UserSchema>; // 単一投稿のスキーマ export const PostSchema = z.object({ id: z.number().int().positive(), userId: z.number().int().positive(), title: z.string().min(1, 'Title cannot be empty'), body: z.string().min(1, 'Body cannot be empty'), }); // ZodスキーマからTypeScript型を推論する export type Post = z.infer<typeof PostSchema>; // ユーザーのリストを含む可能性のあるAPIレスポンスのスキーマ export const UsersResponseSchema = z.array(UserSchema); export type UsersResponse = z.infer<typeof UsersResponseSchema>; // 投稿のリストを含む可能性のあるAPIレスポンスのスキーマ export const PostsResponseSchema = z.array(PostSchema); export type PostsResponse = z.infer<typeof PostsResponseSchema>; // 一般的なエラーレスポンススキーマ (オプションですが、良い習慣です) export const ErrorResponseSchema = z.object({ message: z.string(), code: z.number().optional(), }); export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
ここでは、User
とPost
オブジェクトに対して正確なZodスキーマを定義しました。これには、文字列のmin(1)
、数値のpositive()
、メール形式のemail()
などの検証ルールが含まれます。特に重要なのは、z.infer<typeof Schema>
により、TypeScriptはZodスキーマと完全に一致するインターフェースまたは型エイリアスを自動的に作成できることです。
APIクライアントの構築
次に、これらのスキーマを活用する汎用APIクライアントを作成しましょう。HTTPリクエストにはaxios
を使用します。
// src/apiClient.ts import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import { ZodSchema, z } from 'zod'; import { User, UsersResponse, Post, PostsResponse, ErrorResponseSchema } from './schemas'; // 設定済みのAxiosインスタンスを作成 const api: AxiosInstance = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com', // 例API timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); // リクエストを行い、レスポンスを検証する汎用関数 async function fetchData<T>( url: string, schema: ZodSchema<T> ): Promise<T> { try { const response: AxiosResponse<unknown> = await api.get(url); // 提供されたZodスキーマに対してレスポンスデータを検証する const validatedData = schema.parse(response.data); return validatedData; } catch (error) { if (error instanceof z.ZodError) { // データ検証エラー console.error('APIレスポンス検証失敗:', error.errors); throw new Error(`データ検証エラー: ${error.errors.map(e => e.message).join(', ')}`); } else if (axios.isAxiosError(error)) { // Axios HTTPエラー const axiosError = error as AxiosError; console.error('APIリクエスト失敗:', axiosError.message); if (axiosError.response?.data) { try { // エラーレスポンスデータが存在する場合、解析を試みる const errorResponse = ErrorResponseSchema.parse(axiosError.response.data); console.error('APIエラー詳細:', errorResponse.message); throw new Error(`APIエラー: ${errorResponse.message}`); } catch (parseError) { console.error('APIエラーレスポンスの解析に失敗しました:', parseError); throw new Error(`APIエラー: ${axiosError.response.status} - ${axiosError.message}`); } } throw new Error(`ネットワークエラー: ${axiosError.message}`); } else { // 不明なエラー console.error('予期しないエラーが発生しました:', error); throw new Error('予期しないエラーが発生しました'); } } } // 特定のAPIクライアント関数 export const usersApiClient = { getUsers: (): Promise<UsersResponse> => fetchData<UsersResponse>('/users', UsersResponseSchema), getUserById: (id: number): Promise<User> => fetchData<User>(`/users/${id}`, UserSchema), }; export const postsApiClient = { getPosts: (): Promise<PostsResponse> => fetchData<PostsResponse>('/posts', PostsResponseSchema), getPostById: (id: number): Promise<Post> => fetchData<Post>(`/posts/${id}`, PostSchema), };
fetchData
では、以下のことを行います。
axios
を使用してHTTPリクエストを行います。AxiosResponse<unknown>
は、まだ受信データに信頼を置いていないため、初期段階で使用されます。- 重要なのは、
schema.parse(response.data)
を呼び出すことです。これはZodが、受信データを定義済みのスキーマに対して細心の注意を払って検証する場所です。データが一致しない場合、ZodはZodError
をスローします。 catch
ブロックは、Zod検証エラー、Axios HTTPエラー、その他の予期しない問題などを区別し、特定のエラーメッセージを提供します。- 検証が成功した場合、
z.infer
によりvalidatedData
はT
型であることが保証されます。
型安全なクライアントの利用
このクライアントを利用する now は、非常に安全で直感的です。
// src/app.ts import { usersApiClient, postsApiClient } from './apiClient'; import { User, Post } from './schemas'; // ここで型のみ必要 async function main() { console.log('ユーザーを取得中...'); try { const users: User[] = await usersApiClient.getUsers(); console.log(`${users.length} ユーザーを取得しました。`); // TypeScriptは `users` が `User` オブジェクトの配列であることを認識しています。 // オートコンプリートが機能し、プロパティを誤用するとコンパイル時エラーが発生します。 const firstUser = users[0]; if (firstUser) { console.log(`最初のユーザー: ID=${firstUser.id}, 名前=${firstUser.name}, メール=${firstUser.email}`); // 存在しないプロパティにアクセスしてみる - TypeScriptが警告します! // console.log(firstUser.address.city); // エラー: Property 'address' does not exist on type 'User' } console.log('\n指定の投稿を取得中 (ID 1)...'); const post: Post = await postsApiClient.getPostById(1); console.log(`取得した投稿: タイトル="${post.title}" by User ID ${post.userId}`); // 無効なデータ例 (シミュレートされたシナリオ) // APIが予期せず不正なデータを送信した場合、Zodがそれを捕捉します! // 例えば、'/users' が [{ id: '1', name: 'John Doe' }] を返した場合 // fetchData は 'id' が数値であることを期待しているためZodErrorをスローします。 } catch (error: any) { console.error('データ取得中にエラーが発生しました:', error.message); } } main();
この利用例では、users: User[]
および post: Post
が明示的に型付けされていることに注意してください。これは単なるドキュメントのためではなく、fetchData
関数がZod検証後に T
を返すことが保証されているため、TypeScriptでコンパイル時に強制されます。APIレスポンスがスキーマから逸脱した場合、Zodは、不正なデータがアプリケーションロジックに到達する前にエラーをスローします。
アプリケーションシナリオ
このパターンは、いくつかのシナリオで非常に価値があります。
- 公開API: サードパーティAPIを利用する場合、データ形式に対する制御が少ないため、Zodは予期しない変更や不正なレスポンスに対する重要な防御層を提供します。
- マイクロサービス: マイクロサービスアーキテクチャでは、異なるチームが異なるサービスを所有する可能性があります。Zodスキーマは契約として機能し、サービス間通信が合意されたデータ構造に準拠していることを保証します。
- フロントエンドとバックエンドの分離: フロントエンドとバックエンドのチームが独立して作業する場合、Zodスキーマを共有して型の一貫性を確保し、誤解や統合の問題を減らすことができます。
- データ変換: Zodはデータ変換にも使用でき、スキーマに
.transform()
を追加することで、検証時にデータを整形できます。
結論
静的な型チェックにTypeScriptを、ランタイムデータ検証にZodを統合することで、APIクライアントに比類のないレベルの信頼性を与えます。この相乗効果により、アプリケーションに入るデータは、コンパイル時に構造的に健全であるだけでなく、ランタイムでも期待どおりであることが保証され、データ関連のバグの広範な範囲を防ぎます。APIクライアントを堅牢で保守しやすく、楽しく利用できるものにするために、TypeScriptとZodを採用してください。