PublishedDate: 10 July 2025

客戶端 Nuxt 3 擷取 REST APIs 數據設計

邏輯就是簡單易懂,不要過度的包裝,能用最簡單的就用簡單的完成工作。最好是可替換性,雖然是寫 Vue 專案,同樣的一套程式碼卻不能在 vite, webpack, nuxt 之間相互轉換,這就是 nodejs 專案的宿命。

結論

為了統一所有的程式邏輯,固定將一個 User API 用 $fetch 包裝成 useUsers,分別有 getUser/getUsers 、 createUser 、 updateUser 、 deleteUser 四大類,下次要加入 Message API,也是包裝成 useMessages,在 composables 的程式邏輯是一樣的,這樣閱讀起來也方便許多。另外在使用者介面也是相同的程式邏輯。

nuxt 擷取資料的方法有 $fetch 、 useAsyncData 、 useFetch 三種,要包裝成 REST API 最佳選擇就是 $fetch 。

以下就是使用 $fetch 完整的 User API 支援 get, post, delete, update。

app/composables/useUsers.ts
// app/composables/useUsers.ts
export const useUsers = () => {
  const config = useRuntimeConfig()
  const apiBaseUrl = config.public.apiBaseUrl

  // GET
  const getUser = async (id: number) => {
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/users/${id}`)
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  };

  const getUsers = async (
    offset: number = 0,
    limit: number = 10    
  ) => {
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/users?offset=${offset}&limit=${limit}`);
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  };

  // POST 新增  
  const createUser = async (user: { name: string; email: string }) => {
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/users`, {
        method: 'POST',
        body: user,
      });
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  };

  // PUT
  const updateUser = async (id: number, user: { name: string; email: string }) => {
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/users/${id}`, {
        method: 'PUT',
        body: user,
      });
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  };

  // DELETE
  const deleteUser = async (id: number) => {
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/users/${id}`, {
        method: 'DELETE',
      });
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  };


  const login = async (user) => { 
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/login`, {
        method: 'POST',
        body: user,
      });
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  }

  return { getUser, getUsers, createUser, updateUser, deleteUser, login };
};

流程說明

Vue 先完成客戶端(前端)使用者 UI ,完成後再用 Python Flask 來完成服務器端(後端)的 REST API。前端完成後可以先測試當遇到後端有問題時必要的處置。

原理解釋

主要目的將呼叫 API 都當作使用它,例如呼叫跟 User 相關的就是使用 useUsers ,裡面會提供 get、post、update、delete 等功能;同樣的原理要使用 Message 就是呼叫 useMessages,這樣的好處還有後端有增加修改,前端盡量減少變動。 useUsers 也可以改成 useUserService 。

擷取數據說明

Nuxt 有多種方法取得數據 , useAsyncData 和 useFetch 已經包裝一層,當使用它的時候把放在共用功能元件時候,卻因為錯誤訊息還需要再多加處理,但是衍生出來的問題是數據無法更新,所以最後又多了一個問題。

以下三種程式碼,分別說明回傳資料, useAsyncData 、 useFetch 在 UI 元件中使用很方便,不適合再包裝成 useUsers 。

1.$fetch

$fetch
// $fetch
try {
  const data = await $fetch(`${apiBaseUrl}/api/users`)
} catch (error) {
  const errorData = error 
}
data      // 主機回傳數據  
errorData // 主機回傳的錯誤訊息

2.useFetch

useFetch
// useFetch
const { data, pending, error, refresh } = await useFetch(`${apiBaseUrl}/api/users`)
data.value  // vue ref 物件主機回傳數據
error.value // vue ref 物件不是主機回傳的錯誤訊息
// 需要額外處理主機回傳的錯誤訊息

3.useAsyncData

useAsyncData
// useAsyncData
const { data, pending, error, refresh } = await useAsyncData(
  'name-or-key',
  () => $fetch(`${apiBaseUrl}/api/users`)
)
data.value  // vue ref 物件主機回傳數據
error.value // vue ref 物件不是主機回傳的錯誤訊息
// 需要額外處理主機回傳的錯誤訊息

useAsyncData 是 useFetch 加上 cache 的加強版本。

使用 $fetch

$fetch 包裝 useUsers (最終版本)

composables/useUsers.ts
// 使用 `$fetch` 的 `useUsers` (最終版本) 
// composables/useUsers.ts
export const useUsers = () => {
  const config = useRuntimeConfig()
  const apiBaseUrl = config.public.apiBaseUrl
  // GET
  const getUser = async (id: number) => {
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/users/${id}`)
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  };

  // POST 新增  
  const createUser = async (user: { name: string; email: string }) => {
    let dataResponse = null;
    let errorResponse = null;
    try {
      dataResponse = await $fetch(`${apiBaseUrl}/api/users`, {
        method: 'POST',
        body: user,
      });
    } catch (error) {
      dataResponse = error.data;
      errorResponse = {
        statusCode: error.statusCode,
        message: error.message,
        ...error.data
      }
    }
    return { dataResponse, errorResponse };
  };
  // 完整版看結論
  return { getUser, createUser };
}

使用 useFetch

使用 useFetchuseUsers 在這裡特地命名為

會固定返回多種數據, 當發生錯誤的時候, 會先返回數據, data.value 是 null, error.value 是內建的錯誤訊息, 例如錯誤代碼 400, Bad Request, , 雖然可以用 onResponseError 得到主機給的數據資料, 當 view 接收到第一次的數據時候, 畫面已經顯示出來, 然後 onResponseError 收到數據時更新內容 view 不會再更新畫面, 但是 view 的數據已經更新為新的數據。

composables/useFetchUsers.ts
// 使用 `useFetch` 的 `useUsers` 
// composables/useFetchUsers.ts
export const useUsers = () => {
  const config = useRuntimeConfig()
  const apiBaseUrl = config.public.apiBaseUrl

  // POST 新增
  // 發生錯誤流程順序
  // 1. error.value 已經完成
  // 2. createUser return 
  // 3. view ui 得到數據
  // 4. createUser onResponseError 完成後,產生新的數據
  // 5. view ui 得到數據,但是不會更新 UI,只會顯示第一次得到的數據
  const createUser = async (user: { name: string; email: string }) => {
    console.log("createUser")
    let dataResponse = null;
    let errorResponse = null
    let errorFetch = null;
    const { data, pending, error, refresh } = await useFetch(`${apiBaseUrl}/api/users`, {
      method: 'POST',
      body: user,
      onResponseError({ response }) {
        if (response && response._data) {
          dataResponse = response._data
          console.log(response._data)
          if (response._data.error) {
            console.log(response._data.error)
            errorResponse['error'] = response._data.error;
          } else if (response._data.message) {
            errorResponse['error'] = response._data.message || 'Unknown error';
          }
        }
      },
    });

    if (error.value) {
        console.log(error.value.statusCode)
        console.log(error.value.message)
        errorResponse = {
          statusCode : error.value.statusCode,
          message : error.value.message
        }
    } else if (data.value) {
      dataResponse = data.value
    }
    return { dataResponse, pending, errorResponse, refresh };
  };

  const getUser = async (id: number) => {
    let dataResponse = null;
    let errorResponse = null
    let errorFetch = null;
    const { data, pending, error, refresh } = await useFetch(`${apiBaseUrl}/api/users/${id}`, {
      onResponseError({ response }) {
        if (response && response._data) {
          dataResponse = response._data
          console.log(response._data)
          if (response._data.error) {
            console.log(response._data.error)
            errorResponse['error'] = response._data.error;
          } else if (response._data.message) {
            errorResponse['error'] = response._data.message || 'Unknown error';
          }
        }
      },
    });

    if (error.value) {
        console.log(error.value.statusCode)
        console.log(error.value.message)
        errorResponse = {
          statusCode : error.value.statusCode,
          message : error.value.message
        }
    } else if (data.value) {
      dataResponse = data.value
    }
    return { dataResponse, pending, errorResponse, refresh };
  };

  return { getUser, createUser };
}

使用 useAsyncData

useAsyncData 的流程大約和 useFetch 一樣,不另外說明。

測試 Users API

目前後端程式還沒有完成, useUsers ($fetch) 和 useFetchUsers (useFetch) 得到的資料都會是 error code 500

app/pages/user-demo.vue
<script setup>
// app/pages/user-demo.vue
const { getUser } = useUsers()
const { dataResponse, errorResponse } = getUser(0)


const { getUser: useFetchGetUser } = useFetchUsers()
const { dataResponse: useFetchDataResponse, errorResponse: useFetchErrorResponse  } = useFetchGetUser(0)
</script>

<template>

<div>
  <h2>useUsers</h2>
  <div v-if="errorResponse">
    {{ errorResponse }}
  </div>
  <div v-else>
    {{ dataResponse }}
  </div>
</div>

<div>
  <h2>useFetchUsers</h2>
  <div v-if="useFetchErorResponse">
    {{ useFetchErorResponse }}
  </div>
  <div v-else>
    {{ useFetchDataResponse }}
  </div>
</div>
</template>

設定

最後不要忘記在 nuxt.config.ts 加入 runtimeConfig 用 useRuntimeConfig 使用這些參數。

nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  //...
  runtimeConfig: {
    public: {
      apiBaseUrl: process.env.API_BASE_URL || 'http://127.0.0.1:5525',
    },
  },
  //...
})

參考資料 Reference

Nuxt
Reactive Data Fetching and Updating in Nuxt 3:取得數據與更新,複雜版本的處理方式(英)
API Factories for Vue.js & Nuxt:可以參考組織設計(英)

版本備註

nuxt v3.15.4