😎

Vite+TanstackRouter+Orval+...の環境構築

2025/04/09に公開

はじめに

所属している PlayGround というコミュニティにて、企業協賛のハッカソンに出場しました。我々のチームでは、フロントエンドは Vite×React で web アプリ開発をすることになりました。

フロントエンドの技術構成は以下の通り。

  • フレームワーク: Vite+React
  • UI ライブラリ: shadcn/ui
  • CSS: Tailwind CSS
  • その他: Orval, Tanstack Router

本記事では、そのプロジェクトの環境構築の手順をまとめます。

環境構築手順

1. Vite

Vite は高速な開発環境を提供するビルドツールです。create-react-app がなくなった今、数少ない React プロジェクト作成手段の 1 つです。

1-1. Vite+React プロジェクトを作成

npm create vite@latest client --template react
Need to install the following packages:
  create-vite@6.3.1
Ok to proceed? (y) y
│
◇  Select a framework:
│  React
│
◇  Select a variant:
│  TypeScript
│
◇  Scaffolding project in /Users/luckimai/Desktop/yumemi-pg-hackathon-team3/client...
│
└  Done. Now run:

  cd client
  npm install
  npm run dev

npm notice
npm notice New major version of npm available! 9.8.0 -> 11.2.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.2.0
npm notice Run npm install -g npm@11.2.0 to update!
npm notice

1-2. 不要なファイルの削除

assets, index.css, public/vite.svg , App.css などの初期生成ファイルは要らないので削除します。

2. Tailwind CSS

Tailwind CSS は、ユーティリティファーストの CSS フレームワークです。

2-1. tailwind をインストール

npm install tailwindcss @tailwindcss/vite

2-2. vite.config.ts を編集

import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
  plugins: [tailwindcss()],
});

2-3. tailwind.cssを作成

@import "tailwindcss";

2-4. index.htmlで CSS ファイルを読み込む

<link href="/src/tailwind.css" rel="stylesheet" />

なお、index.html ファイル内で favicon なども設定できます。

3. Shadcn/UI

Shadcn/UI は、再利用可能な UI コンポーネントのコレクションで、Tailwind CSS と組み合わせて使用します。

3-1. IDE がパス解決できるようにtsconfig.jsontsconfig.app.jsonを編集

// tsconfig.json
{
  "files": [],
  "references": [
  ...
  ],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

// tsconfig.app.json
{
  "compilerOptions": {
    // ...
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
    // ...
  }
}

3-2. アプリがパス解決できるように設定

まず、必要なパッケージをインストールします:

npm install -D @types/node

次に、vite.config.tsを以下のように編集します:

import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
});

3-3. shadcn をインストール

npx shadcn@latest init

実行結果:

✔ Preflight checks.
✔ Verifying framework. Found Vite.
✔ Validating Tailwind CSS config. Found v4.
✔ Validating import alias.
✔ Which color would you like to use as the base color? › Neutral
✔ Writing components.json.
✔ Checking registry.
✔ Updating src/tailwind.css
  Installing dependencies.

It looks like you are using React 19.
Some packages may fail to install due to peer dependency issues in npm (see https://ui.shadcn.com/react-19).

✔ How would you like to proceed? › Use --legacy-peer-deps
✔ Installing dependencies.
✔ Created 1 file:
  - src/lib/utils.ts

Success! Project initialization completed.
You may now add components.

4. Tanstack Router

Tanstack Router は、型安全でパフォーマンスに優れた React ルーティングライブラリです。React Router と異なり、ファイルベースのルーティングに対応しているので採用しました。

4-1. 必要なパッケージをインストール

npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/router-devtools

4-2. vite.config.tsを編集

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [
    TanStackRouterVite({ target: "react", autoCodeSplitting: true }),
    react(),
  ],
});

4-3. ルーティング用のページを作成

// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";

export const Route = createRootRoute({
  component: () => (
    <>
      <div className="p-2 flex gap-2">
        <Link to="/" className="[&.active]:font-bold">
          Home
        </Link>
        <Link to="/about" className="[&.active]:font-bold">
          About
        </Link>
      </div>
      <hr />
      <Outlet />
      <TanStackRouterDevtools />
    </>
  ),
});

// src/routes/index.tsx
import { createLazyFileRoute } from "@tanstack/react-router";

export const Route = createLazyFileRoute("/")({
  component: Index,
});

function Index() {
  return (
    <div className="p-2">
      <h3>Welcome Home!</h3>
    </div>
  );
}

// src/routes/about.tsx
import { createLazyFileRoute } from "@tanstack/react-router";

export const Route = createLazyFileRoute("/about")({
  component: About,
});

function About() {
  return <div className="p-2">Hello from About!</div>;
}

4-4. 不要になった App.tsx を削除

4-5. main.tsx を編集

import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";

// Import the generated route tree
import { routeTree } from "./routeTree.gen";

// Create a new router instance
const router = createRouter({ routeTree });

// Register the router instance for type safety
declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}

// Render the app
const rootElement = document.getElementById("root")!;
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
}

4-6. 動作確認

npm run devを実行すると、自動的に routeTree.gen.ts が生成されます。ブラウザで表示されたアプリにホームとアバウトページへのリンクが表示され、クリックでページ移動ができれば成功です。

5. 環境変数のセットアップ

アプリケーションで使用する環境変数を安全に管理するための設定を行います。

5-1. .env.exampleの作成

リポジトリをクローンした際に、cp .env.example .env.localでファイルをコピーして、.env.localに必要な値を入れていきます。

VITE_API_URL= #VITEプレフィックスをつけること
VITE_WS_API_URL=
VITE_USER_POOL_ID=
VITE_USER_POOL_CLIENT_ID=

5-2. vite-env.d.tsの編集

作成した環境変数の型定義を追加します:

/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_APP_URL: string;
  readonly VITE_WS_APP_URL: string;
  readonly VITE_API_CLIENT_ID: string;
  readonly VITE_API_CLIENT_SECRET: string;
}
interface ImportMeta {
  readonly env: ImportMetaEnv;
}

5-3. env.tsを作成

定義した環境変数の型を確定させ、名称を変更します:

import * as z from "zod";
const initEnv = () => {
  const envSchema = z.object({
    API_URL: z.string(),
    WS_API_URL: z.string(),
    USER_POOL_ID: z.string(),
    USER_POOL_CLIENT_ID: z.string(),
  });
  const envVars = {
    API_URL: process.env.VITE_API_URL,
    WS_API_URL: process.env.VITE_WS_API_URL,
    USER_POOL_ID: process.env.VITE_USER_POOL_ID,
    USER_POOL_CLIENT_ID: process.env.VITE_USER_POOL_CLIENT_ID,
  };
  // スキーマでバリデーションしながらパース
  const parsedEnv = envSchema.safeParse(envVars);
  if (!parsedEnv.success) {
    throw new Error("環境変数の設定エラー");
  }
  return parsedEnv.data;
};
export const env = initEnv();

6. Orval

Orval は、OpenAPI(Swagger)スキーマから TypeScript の型定義と API フックを自動生成するツールです。API との連携を型安全に、簡単に行うことができます。

6-1. Orval のインストール

npm i orval -D

6-2. orval.config.tsの作成

import { defineConfig } from "orval";
export default defineConfig({
  waLive: {
    input: {
      target: "../server/http_schema.yml",
    },
    output: {
      mode: "tags-split", // メソッド毎にフォルダを分けてくれる
      clean: true,
      client: "axios", // axios以外にもclientは色々ある。今回はtanstack-query使っていないのでaxios
      target: "src/hooks/orval", // フックの作成先
      override: {
        mutator: {
          path: "src/lib/custom-instance.ts", // custom-instanceを元に作成
          name: "customInstance",
        },
      },
      prettier: true, // 作成したフックに対して自動でprettierを効かせてくれる
    },
  },
});

6-3. custom-instance.tsの作成

Axios インスタンスをカスタマイズして、認証トークンの自動付与やエラーハンドリングを設定します:

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import { toast } from "react-toastify";
import { env } from "../env";

// envを読み込む時はこうする
const { API_URL, USER_POOL_CLIENT_ID } = env;

// Axios インスタンスの作成
const instance: AxiosInstance = axios.create({
  baseURL: API_URL,
  headers: {
    "Content-Type": "application/json",
  },
});

// リクエストインターセプター - すべてのリクエストに認証トークンを追加
instance.interceptors.request.use(
  (config) => {
    // ローカルストレージからアクセストークンを取得
    const userId = localStorage.getItem(
      `CognitoIdentityServiceProvider.${USER_POOL_CLIENT_ID}.LastAuthUser`
    );
    const idToken = localStorage.getItem(
      `CognitoIdentityServiceProvider.${USER_POOL_CLIENT_ID}.${userId}.idToken`
    );
    // トークンが存在する場合、認証ヘッダーに追加
    if (idToken && config.headers) {
      config.headers["Authorization"] = `Bearer ${idToken}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// レスポンスインターセプター - エラーハンドリング
instance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      console.error("認証エラー", error.response);
      toast.error("認証エラーが発生しました。再ログインしてください。");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

// Orval 用のエクスポート関数
export const customInstance = <T>(
  config: AxiosRequestConfig
): Promise<AxiosResponse<T>> => {
  return instance.request<T>(config);
};

6-4. フックの自動生成

OpenAPI スキーマから TypeScript コードを生成するには、以下のコマンドを実行します:

npx orval

また、設定ファイルへのパスを明示的にする場合は、

"orval": "orval --config ./orval.config.ts"

のように script に登録して、npm run orval で実行してやるのがオススメです。
実行後、src/hooks/orvalディレクトリに API クライアントコードが生成されます。

使用する際は、例えば以下のようなスキーマのエンドポイントであれば、

/users/profile/{userId}:
  put:
    tags:
      - Users
    summary: ユーザーのプロフィール登録(初回ログイン時)および更新
    description: 初回ログイン時に追加情報(ユーザー名、プロフィール画像、背景色)を登録・更新。
    parameters:
      - in: path
        name: userId
        schema:
          type: string
        required: true
        description: ユーザーの一意識別子
      - in: header
        name: Authorization
        schema:
          type: string
        required: true
        description: "Authorization: Bearer eyJraWQiOiJrZXkxIiwiYWxnIjoiUlMyNTYifQ...(IDトークン)"
    requestBody:
      description: プロフィール更新情報
      required: true
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/UserProfileUpdate"
    responses:
      "200":
        description: プロフィール登録成功。
UserProfileUpdate:
  type: object
  properties:
    userName:
      type: string
      description: ユーザーの表示名
    profileImage:
      type: string
      description: プロフィール画像のURL
    profileColor:
      type: string
      description: プロフィールの背景色
  required:
    - userName

users フォルダの中の users.ts というファイルを参照し、putUsersProfileUserId(メソッド/カテゴリ/エンドポイント)をインポートすれば良いということになります。

import { getUsers } from "@/hooks/orval/users/users";

const { putUsersProfileUserId } = getUsers();

// paramやrequestBodyはこのように渡す
await putUsersProfileUserId(userId, {
  userName: formData.userName,
  profileImage: formData.profileImage,
  profileColor: formData.profileColor,
});

 
 
 
 
以上です。

KA projects

Discussion