社内向け管理画面では、最初から大規模な構成が必要とは限りません。利用者が限定され、必要な機能が一覧、検索、登録、編集程度なら、Vue 3とViteで軽く始められます。
ただし「小さい画面だから一つのファイルでよい」と進めると、API、フォーム、権限、エラー処理が増えたところで急に保守しにくくなります。
今回作る機能
- 商品一覧
- キーワードと公開状態による検索
- 新規登録と編集
- ローディング、0件、エラー表示
- 削除前の確認
プロジェクト作成
npm create vite@latest light-admin -- --template vue-ts
cd light-admin
npm install
npm run dev初期状態でnpm run buildも実行し、環境問題がないことを確認します。
最小構成
src/
├─ api/products.ts
├─ components/ProductFilter.vue
├─ components/ProductForm.vue
├─ components/ProductTable.vue
├─ composables/useProducts.ts
├─ types/product.ts
└─ views/ProductListView.vue最初から細かく分けすぎる必要はありませんが、API通信、型、表示部品は分離しておくと変更しやすくなります。
型を先に決める
export type ProductStatus = 'draft' | 'published'
export type Product = {
id: number
name: string
price: number
status: ProductStatus
updatedAt: string
}
export type ProductInput = {
name: string
price: number
status: ProductStatus
}一覧用と保存用を分けると、idや更新日時をフォームから誤送信しにくくなります。
APIを画面から分離する
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
export async function getProducts(): Promise<Product[]> {
const response = await fetch(`${API_BASE_URL}/products`, {
credentials: 'include'
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
}Viteのクライアント用環境変数はブラウザへ含まれます。秘密鍵やパスワードは置きません。
一覧状態をまとめる
export function useProducts() {
const products = ref<Product[]>([])
const keyword = ref('')
const status = ref<ProductStatus | 'all'>('all')
const loading = ref(false)
const errorMessage = ref('')
const filteredProducts = computed(() => {
const word = keyword.value.trim().toLowerCase()
return products.value.filter(product => {
const keywordOk =
!word || product.name.toLowerCase().includes(word)
const statusOk =
status.value === 'all' || product.status === status.value
return keywordOk && statusOk
})
})
async function fetchProducts() {
loading.value = true
errorMessage.value = ''
try {
products.value = await getProducts()
} catch (error) {
console.error(error)
errorMessage.value = '商品一覧を取得できませんでした。'
} finally {
loading.value = false
}
}
return {
keyword, status, loading, errorMessage,
filteredProducts, fetchProducts
}
}データが数万件ある場合は、全件取得してブラウザで絞り込まず、検索条件とページ番号をAPIへ送ります。
画面は表示判断に集中する
<template>
<main>
<h1>商品管理</h1>
<ProductFilter v-model:keyword="keyword" v-model:status="status" />
<p v-if="loading">読み込み中です...</p>
<div v-else-if="errorMessage" role="alert">
<p>{{ errorMessage }}</p>
<button type="button" @click="fetchProducts">再読み込み</button>
</div>
<p v-else-if="filteredProducts.length === 0">
条件に一致する商品はありません。
</p>
<ProductTable v-else :products="filteredProducts" />
</main>
</template>ローディング、失敗、0件は別の状態です。一つにすると利用者が待つべきか、条件を変えるべきか分かりません。
フォームと権限
保存中は二重送信を防ぎます。
<button type="submit" :disabled="submitting">
{{ submitting ? '保存中...' : '保存' }}
</button>フロント側の入力チェックは使いやすさのために必要ですが、サーバー側の検証を置き換えるものではありません。
また、ボタンを隠すだけでは権限制御になりません。削除や更新が可能かどうかはAPI側で認証情報を検証します。Cookie認証ならCSRF、SameSite、CORS、credentialsも一緒に確認します。
小規模でも必要なテスト
- 一覧取得成功、0件、500エラー
- 前後空白や長い検索語
- 登録・編集の入力エラー
- 保存ボタンの連打
- セッション切れ
- 権限のない操作
- 長い商品名と大きな数値
- スマートフォン幅とキーボード操作
本番前
npm run build
npm run preview開発サーバーで動いていても、本番のベースパスやAPI URLで問題が出ることがあります。サブディレクトリ配置ならViteのbaseも確認します。
まとめ
Vue 3とViteなら小規模管理画面を早く始められます。ただし、軽く作ることと、すべてを一つのコンポーネントへ入れることは同じではありません。
型、API通信、状態、表示部品の責任を最低限分け、エラーや権限も最初から通常の状態として扱うと、後から機能が増えても修正しやすくなります。

