🚴
僕が考えるフロントエンドの APIクライアント設計:型安全で柔軟なfetchの共通化
Next.jsのAPI呼び出し、もっとスマートにしませんか?
Next.jsでAPI呼び出しを共通化すると、コードがシンプルになり、開発効率が格段に上がります。この記事では、僕がこだわったAPIクライアントの実装を紹介します。特に、次の2点に力を入れました:
• 型安全な戻り値:Result<T>型で成功/失敗を明確にし、TypeScriptの型推論で開発を快適に。
• 関数のみのエクスポート:getやpostを直接エクスポートし、シンプルなインターフェースを提供。
さらに、以下の点も意識しました:
• fetchの隠蔽:URL構築やエラーハンドリングを内部で完結。
• クライアント/サーバー両対応:Next.jsのクライアントコンポーネントでもサーバーコンポーネントでも使える。
• キャメルケース変換:バックエンドのsnake_caseをフロントエンドのcamelCaseに統一。
このAPIクライアントを使えば、API呼び出しがスッキリし、型安全でメンテナンスしやすいコードになります。早速、コードを見ていきましょう!
ソースコード
type CamelCase<T> =
T extends Record<string, unknown>
? { [K in keyof T]: CamelCase<T[K]> }
: T extends (infer U)[]
? U extends Record<string, unknown>
? CamelCase<U>[]
: T
: T;
const toCamelCase = <T extends Record<string, unknown>>(obj: T): CamelCase<T> => {
// MEMO: 配列ならば、その要素を再帰的にキャメルケースに変換する
if (Array.isArray(obj)) {
return obj.map(toCamelCase) as unknown as CamelCase<T>;
// MEMO: オブジェクトならば、そのプロパティのキーを再帰的にキャメルケースに変換する
} else if (obj !== null && typeof obj === 'object') {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key.replace(/([-_][a-z])/g, (group) =>
group.toUpperCase().replace('-', '').replace('_', ''),
),
toCamelCase(value as Record<string, unknown>),
]),
) as unknown as CamelCase<T>;
}
return obj as CamelCase<T>;
};
type Result<T, Error> =
| { ok: true; value: T; response: Response }
| { ok: false; error: Error; response: Response };
const getUrl = (endpoint: string, params?: Record<string, unknown>): URL => {
const url = new URL("example.com" + endpoint);
if (params) {
Object.keys(params).forEach((key) => url.searchParams.append(key, String(params[key])));
}
return url;
};
const getCookies = async (): Promise<string> => {
if (typeof window !== 'undefined') {
return Promise.resolve(document.cookie);
} else {
try {
const { cookies } = await import('next/headers');
const cookieStore = await cookies();
return cookieStore.toString();
} catch (error) {
console.error('Error getting cookies:', error);
return '';
}
}
};
const getCommonHeaders = async (): Promise<Record<string, string>> => {
const cookieString = await getCookies();
return {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
Cookie: cookieString,
};
};
const request = async <T extends object>(
endpoint: string,
method: string,
params?: Record<string, unknown>,
): Promise<Result<T, Error>> => {
const url = getUrl(endpoint, method === 'GET' ? params : undefined);
let body = null;
if (method !== 'GET') {
body = params;
}
const headers = await getCommonHeaders();
try {
const response = await fetch(url, {
method,
headers,
...(body ? { body: JSON.stringify(body) } : {}),
});
if (!response.ok) {
const error = new Error(`HTTP status code: ${response.status} ${response.statusText}`);
return { ok: false, error, response };
}
const data = (await response.json()) as T;
return {
ok: true,
value: toCamelCase(processedData as Record<string, unknown>) as T,
response,
};
} catch (error) {
if (error instanceof Error) {
return { ok: false, error, response: new Response() };
}
// MEMO: typescriptはcatch節での引数の型がunknownになるため、Error型以外はここでthrowする
throw error;
}
};
export const api = {
get: async <T extends object>(
endpoint: string,
params?: Record<string, unknown>,
): Promise<Result<T, Error>> => request<T>(endpoint, 'GET', params),
post: async <T extends object>(
endpoint: string,
params?: Record<string, unknown>,
): Promise<Result<T, Error>> => request<T>(endpoint, 'POST', params)
}
- 呼び出し方
import { api } from '@/api/api-client';
interface Response {
user: User;
}
export const getUser = async (id: number): Promise<User[] | undefined> => {
const result = await api.get<Response>(`/api/v1/user/${id}`);
if (result.ok) {
return result.value.user;
} else {
console.error(result.error.message);
return undefined;
}
};
より快適なNext.js開発のために
本記事では、Next.jsにおけるAPI呼び出しを共通化し、開発体験を向上させるためのAPIクライアント実装をご紹介しました。
このクライアントが目指したのは、以下の実現です。
- Result<T>型による明確な成功/失敗の表現と、TypeScriptの強力な型推論による型安全性の確保。
- getやpostメソッドを直接エクスポートすることによる、シンプルで直感的なインターフェース。
- URL構築や共通ヘッダーの設定、エラーハンドリングといった煩雑な処理の隠蔽。
- クライアントコンポーネントとサーバーコンポーネントの両方で動作する柔軟性。
- バックエンドのsnake_caseからフロントエンドのcamelCaseへの自動変換による記述の一貫性。

株式会社ウェイブのエンジニアによるテックブログです。 弊社では、電子コミック、アニメ配信などのエンタメコンテンツを自社開発で運営しております! wwwave.jp/service/
Discussion