この記事はジュニアエンジニア向けの記事になります。
ざっくりいうと、フロントエンドからバックエンドのエンドポイントにリクエストを送りレスポンスを受け取るという、基本的な通信において、API Clientを作ってそこに型を当てるというものです。
API Clientとは?
API Client(エーピーアイ クライアント)とは、APIに対してリクエストを送信し、レスポンスを受け取るためのソフトウェアまたはライブラリのことを指します。
私はこんな感じで書いています。(型追加前の状態です。)
export const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:5000';
export const apiClient = {
get: async (url: string, options: RequestInit = {}) => {
const res = await fetch(backendUrl + url, {
...options,
method: 'GET',
});
return handleResponse(res);
},
post: async (url: string, body: Record<string, unknown>, options: RequestInit = {}) => {
const res = await fetch(backendUrl + url, {
...options,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
body: JSON.stringify(body),
});
return handleResponse(res);
},
// 他のHTTPメソッドも必要に応じて追加
};
async function handleResponse(res: Response) {
if (!res.ok) {
const error = await res.json();
return { success: false, error: error.message || 'APIリクエストに失敗しました' };
}
return res.json();
}
こんな感じで呼び出して使用
// app/serverActions.js
"use server";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { apiClient } from "@/app/lib/apiClient";
// 認証チェック用のアクション
export async function checkAuth() {
try {
const cookieStore = await cookies();
const authToken = cookieStore.get('authToken');
const userId = cookieStore.get('userId');
const backendUrl = getBackendUrl();
if (!authToken) {
redirect('/login');
}
const res = await apiClient.get(`/api/auth/check`, {
headers: {
"Content-Type": "application/json",
"Cookie": `authToken=${authToken?.value || ''}`
},
cache: 'no-store' // キャッシュを無効化
});
if (res.user.id !== userId?.value) {
throw new Error(res.message || "認証エラーが発生しました");
}
return res;
} catch (error) {
console.error("認証チェックエラー:", error);
throw error;
}
}
最終的なゴール
型を下記のように当てはめる
型に合わない、リクエストやレスポンスがあればエラーになる。
import { paths } from '@/app/types/api';
export const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:5000';
export const apiClient = {
get: async <T extends keyof paths>(url: T, options: RequestInit = {}) => {
const res = await fetch(backendUrl + url, {
...options,
method: 'GET',
});
return handleResponse<paths[T]['get']['responses']['200']['content']['application/json']>(res);
},
post: async <T extends keyof paths>(url: T, body: paths[T]['post']['requestBody']['content']['application/json'], options: RequestInit = {}) => {
const res = await fetch(backendUrl + url, {
...options,
method: 'POST',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
body: JSON.stringify(body),
});
return handleResponse<paths[T]['post']['responses']['200']['content']['application/json']>(res);
},
};
ここに、型を書いたyamlを用意する必要がある。
import { paths } from '@/app/types/api';
openapi: 3.0.0
info:
title: User API
version: 1.0.0
description: ユーザー管理APIのドキュメント
contact:
name: APIサポート
url: http://www.example.com/support
email: support@example.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
tags:
- name: User
description: ユーザー関連の操作
- name: Collection
description: コレクション関連の操作
- name: Question
description: 質問関連の操作
- name: Progress
description: 進捗関連の操作
- name: Result
description: 結果関連の操作
- name: Auth
description: 認証関連の操作
paths:
/:
get:
tags:
- Home
summary: ホーム画面
description: ホーム画面を表示します。
operationId: getHome
responses:
'200':
description: ホーム画面
content:
application/json:
schema:
type: object
/auth/login:
post:
tags:
- Auth
summary: ログイン
description: ログインします。
operationId: login
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email:
type: string
format: email
password:
type: string
format: password
responses:
'200':
description: ログイン成功
content:
application/json:
schema:
type: object
properties:
token:
type: string
description: トークン
user:
type: object
$ref: '#/components/schemas/User'
'401':
description: ログイン失敗
'404':
description: ユーザーが見つかりません
'500':
description: サーバーエラー
'400':
description: バリデーションエラー
content:
application/json:
schema:
type: object
properties:
message:
type: string
なぜapiClientに型を当てはめる必要があるのか?
それはよりセキュアにするためです。なんでも送信okなんでも受け取りokとなれば、それだけセキュリティの脆弱性が増します。よりセキュアにするためこの型が必要なのです。
続きは第二回で!