@i.un/nuxt-api
v2.0.3
Published
A robust, production-ready API client module for Nuxt 4. Designed for enterprise-grade authentication handling, SSR compatibility, and seamless token refreshing.
Maintainers
Readme
@i.un/nuxt-api
面向 Nuxt 4 的生产级 API 客户端模块,专注于企业级鉴权处理、SSR 兼容与无感 Token 刷新。
A robust, production-ready API client module for Nuxt 4. Designed for enterprise-grade authentication handling, SSR compatibility, and seamless token refreshing.
特性 / Features
🔐 智能 Token 刷新 / Smart Token Refresh
- 自动拦截 401 并刷新 Token。 Automatic 401 interception and token refreshing.
- 请求排队:避免并发触发多次刷新,挂起的请求在拿到新 Token 后自动重放。 Request Queueing: Prevents multiple refresh requests; pending requests retry automatically with the new token.
- SSR 支持:在 SSR 与后端 API 之间正确透传 Cookie。 SSR Support: Perfectly handles cookie passing between SSR server and backend API.
- 跨域 / 子域支持:自动识别并修正 Cookie 域,实现跨子域无感鉴权。 Cross-Domain & Subdomain Support: Auto-detects and fixes cookie domains for seamless cross-subdomain authentication.
🧬 类型化路径绑定(可选)/ Typed Path Binding (optional)
- 基于
openapi-typescript生成的paths类型,把 URL → query/body/path/响应类型 一次性绑定到位。 Bind URL → query/body/path/response types directly from anopenapi-typescriptpathsdefinition. - 路径模板(
/users/{id})自动插值;当 spec 声明了 path 参数时,path字段会变为必填。 Path templates (/users/{id}) auto-interpolated;pathbecomes required when the spec declares one. - URL 前缀和信封字段名可配置,库本身不假设任何项目约定。
Configurable URL prefix and envelope
datakey — zero project assumptions baked in.
- 基于
🛡 安全优先 / Security First
- 尊重
HttpOnlyCookie。 RespectsHttpOnlycookies. - 不会覆盖后端设置的安全标志,除非必要。 Does not overwrite backend security flags unless necessary.
- 尊重
🛠 开发者友好 / Developer Friendly
- 完整 TypeScript 支持。 Full TypeScript support.
- 可配置的 Debug 日志。 Configurable Debug Logging.
- 可选择返回完整信封或仅返回业务数据。 Return full envelope or just unwrapped data.
安装 / Installation
# npm
npm install @i.un/nuxt-api
# pnpm
pnpm add @i.un/nuxt-api
# yarn
yarn add @i.un/nuxt-api配置 / Configuration
在 nuxt.config.ts 中注册模块:
Add the module to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['@i.un/nuxt-api'],
iunApi: {
// 后端 API 地址 / Backend API URL
apiBase: 'https://api.example.com',
// 可选:SSR 专用地址(例如 Docker 内网)
// Optional: Different URL for SSR (e.g. internal docker network)
apiBaseServer: 'http://backend-service:8080',
// 鉴权 / Authentication
cookieName: 'access_token', // 默认值 / Default
// 接口路径 / Endpoints
endpoints: {
// 刷新端点。设 false 禁用,或填路径(如 '/auth/refresh')。
// 触发刷新的业务码 unauthorizedCode、token 提取容器 dataKey 在 useApi options 里配。
// Refresh endpoint. false to disable, or a path (e.g. '/auth/refresh').
// The trigger code (unauthorizedCode) and token container (dataKey) live in useApi options.
refresh: false, // 默认 / default
},
// 跳转 / Redirects
redirects: {
// 设为 false 禁用跳转,或填入自定义登录路径(如 '/auth/login')
// Set to false to disable redirect,
// or provide a custom redirect path (e.g. '/auth/login')
login: false, // 默认 / default
},
// 调试 / Debugging
debug: process.env.NODE_ENV === 'development',
},
});使用 / Usage
1. 基础请求 / Basic Requests (useApi)
useApi 在 Nuxt 的 $fetch 之上封装了自动鉴权与错误处理,默认如实返回后端响应(不解包)。所有参数(query / body / path / headers / timeout …)统一放在调用 options 对象里。
useApi wraps Nuxt's $fetch with auto-auth and error handling, and returns the backend response as-is by default (no unwrapping). All knobs (query, body, path, headers, timeout, …) live in a single call options object.
<script setup lang="ts">
const api = useApi();
// GET 请求,默认如实返回(不解包);解包是 opt-in,见下方「类型化 API」
// GET request — returns as-is by default; unwrapping is opt-in (see Typed API).
const users = await api.get<User[]>('/users');
// POST 请求(body 放在 options 里)
// POST request (body in options).
await api.post('/posts', { body: { title: 'Hello' } });
// Query + 原生 $fetch 选项一并传入
// Query params + native $fetch options.
const profile = await api.get<Profile>('/profile', {
query: { include: 'avatar' },
headers: { 'X-Custom': 'value' },
timeout: 5000,
});
</script>2. 完整信封 vs 解包 / Full envelope vs unwrap
默认不解包 —— api.get(...) 直接拿到后端完整响应(含 code / message / data)。要让库自动剥出 data,在 wrapper 里显式开启解包(下一节);开启解包后仍想拿完整信封,就再建一个不解包的 wrapper。
No unwrap by default — api.get(...) gives you the full backend response (code / message / data). To have the library peel out data, opt in in your wrapper (next section); if you still need full envelopes after that, make a second no-unwrap wrapper.
信封字段名与码值都可在
useApioptions 里配,默认{ code, message, data }+ 成功码0/ 未授权码401:codeKey/messageKey/dataKey/successCode/unauthorizedCode。后端用别的约定时覆盖对应项即可。Envelope field names and codes are all configurable in
useApioptions — defaulting to{ code, message, data }with success0/ unauthorized401:codeKey/messageKey/dataKey/successCode/unauthorizedCode. Override whichever ones your backend differs on.
3. 类型化 API / Typed API with OpenAPI
用 openapi-typescript 生成 paths 类型,把它作为第一个泛型传给 useApi。endpoint 字符串、query / body / path、以及响应类型,全部从 spec 推断 —— 不再需要每次手写 <T>。
Generate a paths type with openapi-typescript, then pass it as the first generic to useApi. The endpoint string, query, body, path, and the response type all flow from the spec — no per-call <T> needed.
pnpm dlx openapi-typescript http://localhost:3000/api/openapi.json -o types/openapi.d.ts// composables/useApi.ts —— 在项目里只定义一次的 wrapper
// composables/useApi.ts — your project's wrapper, defined once.
import type { paths } from '~/types/openapi';
// 泛型:<Paths, Prefix, DataKey>
// Prefix —— 从 spec 路径里剥掉的前缀(默认 '')
// DataKey —— never(默认) = 不解包;传字段名(如 'data') = 解包该字段,
// 且 options.dataKey 必须 === DataKey(类型强制一致)
// Generics: <Paths, Prefix, DataKey>
// Prefix — string segment stripped from spec paths (default '').
// DataKey — never (default) = no unwrap; a field name = unwrap it,
// and options.dataKey must === DataKey (type-enforced).
// 不解包(默认):useApi<paths, '/api'>()
// 解包信封 data:显式传 dataKey,类型强制与 DataKey 一致 ↓
export const useTypedApi = () => useApi<paths, '/api', 'data'>({ dataKey: 'data' });<script setup lang="ts">
const api = useTypedApi();
// endpoint 自动补全;响应类型自动推断
// Endpoint autocompletes from spec; response type is inferred.
const users = await api.get('/users');
// ^? User[]
// path 参数:spec 声明了路径参数时,path 字段变为必填
// Path params: `path` becomes required when the spec declares one.
const user = await api.get('/users/{id}', { path: { id: '42' } });
// ^? User
// body 与 query 同样具有类型
// Body and query are also typed.
await api.post('/posts', {
body: { title: 'Hello', tags: ['nuxt'] },
});
</script>项目场景 / Project scenarios
| 后端约定 / Backend | 推荐写法 / Generic call |
| ---------------------------------------------------- | -------------------------------------------- |
| 无前缀、无信封(裸响应,默认)/ raw, no envelope (default) | useApi<paths, '/api'>() |
| NestJS 全局 /api + data 信封(解包) | useApi<paths, '/api', 'data'>({ dataKey: 'data' }) |
| 版本化 /v1/... + payload 信封(解包) | useApi<paths, '/v1', 'payload'>({ dataKey: 'payload' }) |
| 未类型化(旧代码 / 混合)/ Untyped (legacy / mixed) | useApi() —— 任意 string,用 <T> 指定返回 |
类型工具集 / Type Kit Pattern
useApi wrapper 解决「调用」处的类型推断;当业务侧需要显式标注响应/请求类型时(例如声明 export type UserBalance = …),用库导出的类型原语在项目里组装一组短别名。两组工具共用同一份 <Paths, Prefix, DataKey> 配置,保持类型口径一致。
The useApi wrapper handles inference at call sites. When you need to declare response/request types explicitly (e.g. export type UserBalance = …), use the type primitives exported by the library to build a small set of short aliases in your project. Both layers share the same <Paths, Prefix, DataKey> constants, so the type story stays consistent.
库导出的原语 / Exported primitives:
import type {
// 给定 (path, method) 推导 / derived from (path, method):
EndpointFor, // 含某 method 的路径并集 / paths union for a given method
OperationFor, // 单个 operation / one operation
QueryFor, // query 类型 / query type
BodyFor, // body 类型 / body type
PathParamsFor, // path 参数类型 / path params type
ResponseFor, // 解包后的响应类型 / unwrapped response type
OptionsArgsFor, // 可变参数元组 / rest options tuple
// HTTP method 调用签名 / HTTP method call signature:
ApiMethod, // 单个 method 的完整函数签名 / full function signature for one method
// 独立工具 / standalone helpers:
StripPrefix, // 剥前缀映射 / strip-prefix mapping
CallOptions, // useApi 调用选项 / call options
RequiredPathCallOptions, // path 必填变体 / variant with required path
} from '@i.un/nuxt-api';项目侧的"类型 kit"(与 wrapper 同一份配置常量):
The project-side "type kit" (using the same constants as the wrapper):
// types/api-kit.ts —— 在项目里只定义一次
// types/api-kit.ts — defined once per project.
import type { paths } from '~/types/openapi';
import type {
ResponseFor,
BodyFor,
QueryFor,
PathParamsFor,
StripPrefix,
} from '@i.un/nuxt-api';
// 复用库工具剥掉与 wrapper 一致的前缀
// Reuse the library's strip-prefix util so the alias surface matches the wrapper.
type ApiPaths = StripPrefix<paths, '/api'>;
/** ApiResponse<'/me/credits'> → 解包后的响应类型 / unwrapped response type */
export type ApiResponse<
P extends keyof ApiPaths,
M extends keyof ApiPaths[P] & string = 'get',
> = ResponseFor<paths, '/api', P, M, unknown, 'data'>;
/** ApiBody<'/posts', 'post'> → 请求 body 类型 / request body type */
export type ApiBody<
P extends keyof ApiPaths,
M extends keyof ApiPaths[P] & string = 'post',
> = BodyFor<paths, '/api', P, M>;
/** ApiQuery<'/users'> → query 参数类型 / query params type */
export type ApiQuery<
P extends keyof ApiPaths,
M extends keyof ApiPaths[P] & string = 'get',
> = QueryFor<paths, '/api', P, M>;
/** ApiPathParams<'/users/{id}'> → path 参数类型 / path params type */
export type ApiPathParams<
P extends keyof ApiPaths,
M extends keyof ApiPaths[P] & string = 'get',
> = PathParamsFor<paths, '/api', P, M>;业务侧使用(与后端 schema 名解耦,不必关心 DTO 命名):
Usage in business code (decoupled from backend schema names):
import type { ApiResponse, ApiBody } from '~/types/api-kit';
// 响应类型从 URL 派生
// Response type derived from URL.
export type UserBalance = ApiResponse<'/me/credits'>;
export type TransactionList = ApiResponse<'/me/credits/transactions'>;
export type CreditTransaction = NonNullable<TransactionList['items']>[number];
// 请求体类型也从 spec 派生
// Request body types come from the spec too.
export type CreateRechargeDto = ApiBody<'/me/credits/recharges'>;为什么不把这些短别名直接放进库 —— ApiResponse<P> 隐含三个项目侧约定(Prefix / DataKey / 默认 method),把它做进库就需要再开 3 个泛型槽,项目侧用前还要再 wrap 一层,反而更绕。把"用一次项目常量"留在项目侧,正好和 wrapper composable 对称:一处定义、两处复用。
Why these aliases aren't shipped from the library directly — ApiResponse<P> bakes in three project-level decisions (Prefix / DataKey / default method). Shipping it would force 3 more generic slots and an extra project-side wrap. Keeping the "apply project constants once" step in userland keeps it symmetric with the wrapper composable: one place defines the constants, two places consume them.
信封 / 非信封混用 / Mixed envelope / non-envelope
像 /health、/version 这类接口往往返回裸 JSON,没有信封。解包过程用 keyof 做了守卫:
Some endpoints (/health, /version, …) return raw JSON without an envelope. The unwrap step uses a keyof guard, so:
- 响应中包含
DataKey字段 → 解包成该字段的类型。 Responses with theDataKeyfield → unwrapped to that field's type. - 响应中不包含 → 原样返回,类型不丢精度。 Responses without it → returned as-is with full type precision.
const me = await api.get('/me'); // User(从信封中解包 / unwrapped from envelope)
const health = await api.get('/health'); // { status: 'ok' | 'down' }(裸响应 / raw)非 JSON 响应 / Non-JSON responses
ofetch 按响应的 content-type 自动解析:text/* 成 string(text/event-stream 除外,是流),二进制成 Blob;解包遇到非对象响应原样返回。类型跟着 spec 走 —— 声明了 text/* 的接口(如 CSV 导出)直接推成 string,不用 as;二进制给 unknown。
ofetch parses by the response content-type: text/* into a string (except text/event-stream, a stream), binary into a Blob; unwrapping returns non-object responses as-is. Types follow the spec — an endpoint declaring text/* (e.g. a CSV export) is typed as a string, no as; binary gives unknown.
const csv = await api.get('/contacts/export', { query: { format: 'csv' } }); // string要按次改解析方式,透传 ofetch 的 responseType('blob' / 'text' / …);它不改返回类型,需自行 as。
To override parsing per call, pass ofetch's responseType ('blob' / 'text' / …); it doesn't change the return type, so cast it yourself.
配置一致性 / Configuration consistency
类型层的 Prefix 泛型必须与模块运行时的 apiBase(路径部分)保持一致 —— 两者错配属于配置 bug,运行时不做校验。DataKey 与 options.dataKey 则由类型强制一致(DataKeyOption),传错会直接编译报错,无需手动对齐。
The type-level Prefix generic must match the runtime apiBase (path portion) — a mismatch is a configuration bug, not checked at runtime. DataKey and options.dataKey, by contrast, are type-enforced to match (DataKeyOption): getting them out of sync is a compile error, so no manual alignment is needed.
4. 手动 Token 管理 / Manual Token Management (useTokenRefresh)
通常无需手动操作,库会自动处理;如需手动控制可调用。参数 dataKey 指明后端 refresh 响应里 token 所在的容器字段(如 'data');token 在顶层、或响应本身就是 token 串时可省略。
Usually handled automatically, but exposed if needed. The dataKey argument tells it which container field holds the token in the refresh response (e.g. 'data'); omit it when the token sits at the top level or the response is the raw token string.
const { refreshToken } = useTokenRefresh('data');
const doRefresh = async () => {
const newToken = await refreshToken();
if (newToken) {
console.log('Token refreshed manually');
}
};许可证 / License
MIT
