0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OpenAPI GeneratorでAPI Clientと型を作っていく 第一回

Posted at

この記事はジュニアエンジニア向けの記事になります。
ざっくりいうと、フロントエンドからバックエンドのエンドポイントにリクエストを送りレスポンスを受け取るという、基本的な通信において、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となれば、それだけセキュリティの脆弱性が増します。よりセキュアにするためこの型が必要なのです。

続きは第二回で!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?