Vue 基础体系 · 第 66/70 篇。示例基于 Vue 3、Composition API、TypeScript 与现代 Vite 工具链;版本敏感能力会单独标注。

Nuxt Server Routes:Nitro、API、运行时配置、缓存和部署

Nuxt 的服务端能力不是“在 Vue 页面旁边放几个接口文件”这么简单。一个请求进入 Nuxt 后,可能经过 Nitro 的路由匹配、服务端中间件、API Handler、运行时配置读取、缓存层和部署适配器,最后才生成 HTTP 响应。

理解这条链路,至少需要区分四个概念:

  • Server Routes:Nuxt 项目中由文件系统声明的服务端路由。
  • Nitro:Nuxt 使用的服务端运行时和构建系统,负责路由、打包、适配部署平台等。
  • API Routes:通常位于 server/api 下、用于提供数据接口的 Server Routes。
  • 运行时配置与缓存:服务端代码如何读取部署时配置,以及响应在哪一层被复用。

本文示例基于 Nuxt 3、Vue 3、Composition API、TypeScript 和现代 Vite 工具链。Nuxt 和 Nitro 的部分配置会随版本变化,使用时应以当前项目生成的类型和官方文档为准。

一、请求进入 Nuxt 后发生了什么

Nuxt 的服务端请求可以抽象为以下流程:

sequenceDiagram
    participant C as Client
    participant N as Nitro Server
    participant M as Server Middleware
    participant R as Route Matcher
    participant H as Event Handler
    participant D as Data Source

    C->>N: HTTP request
    N->>M: 执行 server/middleware
    M-->>N: 放行或直接返回错误
    N->>R: 根据 URL 和 HTTP Method 匹配
    R->>H: 调用 Server/API Handler
    H->>D: 查询数据库或外部服务
    D-->>H: 返回数据
    H-->>N: Response
    N-->>C: HTTP response

这里的关键对象是 事件对象 event。它不是浏览器事件,而是 H3/Nitro 服务端请求上下文,通常包含:

  • 请求方法、URL、请求头;
  • 路由参数;
  • Cookie;
  • 请求体读取能力;
  • 响应头和响应状态设置能力;
  • 当前请求范围内的上下文数据。

一个最小的服务端路由如下:

// server/api/health.get.ts
export default defineEventHandler(() => {
  return {
    ok: true,
    service: 'web',
  }
})

启动开发服务器:

npm run dev

请求:

curl http://localhost:3000/api/health

预期响应:

{
  "ok": true,
  "service": "web"
}

server/api/health.get.ts 中各部分的含义是:

  • server/:Nuxt 服务端目录;
  • api/:API 路由目录;
  • health:路由名称;
  • .get:只匹配 GET 方法;
  • .ts:TypeScript 实现;
  • defineEventHandler:把函数声明为 Nitro/H3 可调用的事件处理器。

对应路径是 /api/healthserver/api 下的文件默认带 /api 前缀,而 server/routes 下的文件不带这个前缀:

// server/routes/health.get.ts
export default defineEventHandler(() => {
  return { ok: true }
})

该文件对应 /health,而不是 /api/health

1. HTTP 方法和文件名

可以通过文件名后缀约束方法:

server/api/users.get.ts       -> GET /api/users
server/api/users.post.ts      -> POST /api/users
server/api/users.delete.ts    -> DELETE /api/users
server/api/users.ts           -> 多数情况下可处理多个方法

如果一个资源需要分别处理不同方法,推荐使用方法后缀:

// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ name?: string }>(event)

  if (!body.name || body.name.trim().length < 2) {
    throw createError({
      statusCode: 400,
      statusMessage: 'name must contain at least 2 characters',
    })
  }

  return {
    id: crypto.randomUUID(),
    name: body.name.trim(),
  }
})

请求:

curl -X POST http://localhost:3000/api/users \
  -H 'content-type: application/json' \
  -d '{"name":"Ada"}'

预期结果类似:

{
  "id": "生成的 UUID",
  "name": "Ada"
}

readBody 会解析请求体。这里必须提供 content-type: application/json,否则不同运行时或客户端的解析行为可能不符合预期。生产接口还应限制请求体大小、校验字段类型和长度,并避免直接信任客户端传来的 ID、角色或金额。

2. 动态路由和查询参数

动态文件名会生成路由参数:

// server/api/users/[id].get.ts
export default defineEventHandler((event) => {
  const id = getRouterParam(event, 'id')

  if (!id) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Missing user id',
    })
  }

  return {
    id,
    name: 'Example User',
  }
})

请求 /api/users/42 时,id 为字符串 "42"。即使业务上它代表数字,也不能假设框架会自动转换:

const numericId = Number(id)

if (!Number.isInteger(numericId) || numericId <= 0) {
  throw createError({
    statusCode: 400,
    statusMessage: 'Invalid user id',
  })
}

查询参数使用 getQuery

// server/api/search.get.ts
export default defineEventHandler((event) => {
  const query = getQuery(event)
  const keyword = typeof query.q === 'string' ? query.q.trim() : ''

  return {
    keyword,
    page: query.page ?? '1',
  }
})

请求:

curl 'http://localhost:3000/api/search?q=nuxt&page=2'

路径参数和查询参数有不同语义:

  • /api/users/42 中的 42 是资源路径的一部分;
  • /api/users?page=2 中的 page 是查询条件;
  • 查询参数可以重复或具有数组形式,解析后不一定是单个字符串,因此应进行类型收窄。

二、Nitro 到底负责什么

Nitro 是 Nuxt 的服务端引擎。它不等同于某一个 HTTP 业务接口,也不等同于 Node.js。它主要负责:

  1. 收集 server/ 目录中的路由和中间件;
  2. 构建服务端代码;
  3. 提供基于 H3 的请求处理模型;
  4. 处理运行时配置;
  5. 支持缓存、预渲染和路由规则;
  6. 将应用适配为 Node、Serverless、边缘运行时或静态输出等目标。

Nuxt 页面服务端渲染和 server/api 接口通常共享同一个 Nitro 服务,但它们不是同一种路由:

  • 页面路由通常负责返回 HTML 或 Nuxt SSR 响应;
  • API 路由通常返回 JSON、文件、重定向或错误响应;
  • 两者都可以使用服务端运行时能力,但缓存、认证和响应格式可能不同。

1. event 是请求级状态,不是全局状态

可以把请求处理器看成函数:

Response=Handler(Request,RuntimeConfig,ExternalState)Response = Handler(Request, RuntimeConfig, ExternalState)

其中:

  • Request 是当前 HTTP 请求;
  • RuntimeConfig 是当前进程可用的部署配置;
  • ExternalState 是数据库、缓存、文件系统或外部服务;
  • Response 是最终 HTTP 响应。

event 只表示当前请求,不应被保存到模块级变量中:

// 错误示例
let currentEvent: unknown

export default defineEventHandler((event) => {
  currentEvent = event
  return { ok: true }
})

多个请求可能并发执行。把请求对象存入全局变量会造成请求之间的数据覆盖、用户信息串读或内存泄漏。需要跨请求共享的数据,应使用明确的外部存储或只读进程级配置;需要当前请求范围的数据,应放在 event.context 中。

例如,认证中间件可以把用户信息挂到当前请求上下文:

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  const token = getCookie(event, 'session')

  if (!token) {
    return
  }

  // 实际项目中应验证签名或查询会话存储
  event.context.user = {
    id: 'user-1',
    role: 'user',
  }
})

路由处理器再读取:

// server/api/me.get.ts
export default defineEventHandler((event) => {
  const user = event.context.user

  if (!user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized',
    })
  }

  return user
})

这只是展示数据流。生产环境不能仅凭 Cookie 中存在一个字符串就认为用户已认证,必须验证会话签名、令牌有效期和撤销状态。

2. 中间件、路由和错误的关系

server/middleware 中的处理器会在服务端路由匹配后、具体处理器执行前参与请求流程。它常用于:

  • 记录请求;
  • 注入请求上下文;
  • 做统一认证;
  • 设置通用响应头。

如果中间件抛出错误,后续路由不会执行:

export default defineEventHandler((event) => {
  const internalKey = getHeader(event, 'x-internal-key')

  if (internalKey !== process.env.INTERNAL_KEY) {
    throw createError({
      statusCode: 403,
      statusMessage: 'Forbidden',
    })
  }
})

但不应把所有逻辑都塞进全局中间件。只需要保护某个接口时,在具体路由中检查更容易理解,也能避免健康检查、静态资源或公开接口被误拦截。

统一错误应使用 createError

throw createError({
  statusCode: 404,
  statusMessage: 'User not found',
  data: {
    code: 'USER_NOT_FOUND',
  },
})

不要把数据库异常、令牌内容或堆栈直接返回给客户端。服务端日志可以记录完整错误,客户端响应应只包含可公开的信息。

三、Nuxt API 与客户端调用

客户端调用 Nuxt API 通常使用 $fetch

<script setup lang="ts">
const { data, error, pending } = await useFetch('/api/health')
</script>

<template>
  <p v-if="pending">Loading...</p>
  <p v-else-if="error">Request failed</p>
  <pre v-else>{{ data }}</pre>
</template>

useFetch 是 Nuxt 对数据获取、SSR 和响应式状态的封装。服务端渲染时,它可以参与 Nuxt 的数据传输流程,避免浏览器再次无意义地重复获取同一份 SSR 数据。

直接使用 $fetch 更适合事件处理器或明确的一次性请求:

<script setup lang="ts">
async function createUser() {
  const result = await $fetch('/api/users', {
    method: 'POST',
    body: {
      name: 'Ada',
    },
  })

  console.log(result)
}
</script>

需要区分两类请求:

// 服务器内部调用 API
await $fetch('/api/users')

// 服务端直接调用业务函数
await userService.list()

在服务端渲染期间,直接调用同一个 Nuxt API 可能导致一次额外的 HTTP 风格处理。对于复杂系统,通常更适合把核心业务抽到普通 TypeScript 服务模块中,让 API Handler 和页面数据加载共同调用它:

// server/services/userService.ts
export async function listUsers() {
  return [
    { id: '1', name: 'Ada' },
    { id: '2', name: 'Grace' },
  ]
}
// server/api/users.get.ts
import { listUsers } from '../services/userService'

export default defineEventHandler(() => {
  return listUsers()
})

这样可以避免把“内部模块调用”和“HTTP API 调用”混为一谈。HTTP 层负责认证、参数解析和响应状态;业务模块负责领域逻辑。

四、运行时配置:构建时配置和部署时配置不是一回事

Nuxt 配置中常见两类值:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    databaseUrl: '',
    apiSecret: '',

    public: {
      apiBase: '/api',
      appName: 'Demo',
    },
  },
})

runtimeConfig 的规则是:

  • runtimeConfig 顶层值只应在服务端访问;
  • runtimeConfig.public 中的值会暴露给客户端;
  • 密钥、数据库地址、签名密钥等不能放入 public
  • 配置默认值会参与构建,但生产值可以通过符合规则的环境变量覆盖。

服务端读取配置:

// server/api/config-check.get.ts
export default defineEventHandler((event) => {
  const config = useRuntimeConfig(event)

  return {
    hasDatabaseUrl: Boolean(config.databaseUrl),
    apiBase: config.public.apiBase,
  }
})

客户端读取公开配置:

<script setup lang="ts">
const config = useRuntimeConfig()

console.log(config.public.apiBase)
</script>

不要这样做:

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      apiSecret: 'secret-value',
    },
  },
})

因为 public 配置会进入客户端可访问的数据。即使代码中没有主动显示它,浏览器仍可能通过构建产物、Nuxt payload 或运行时配置看到它。

1. 环境变量覆盖

Nuxt 对运行时配置的环境变量覆盖通常使用 NUXT_ 前缀,并根据配置层级映射。例如:

NUXT_DATABASE_URL='postgres://user:pass@db/app'
NUXT_API_SECRET='production-secret'
NUXT_PUBLIC_API_BASE='https://example.com/api'

部署时:

NUXT_DATABASE_URL='postgres://user:pass@db/app' \
NUXT_API_SECRET='production-secret' \
node .output/server/index.mjs

这里有两个容易混淆的事实:

  1. nuxt.config.ts 本身会在构建阶段执行;
  2. runtimeConfig 的设计目标是让服务端在运行阶段读取部署配置。

因此不应把生产密钥写死在仓库,也不能把普通的 process.env.SOME_KEY 使用方式等同于 Nuxt 运行时配置。对于需要被 useRuntimeConfig() 正确覆盖的字段,应按 Nuxt 约定设置 NUXT_... 环境变量。

运行时配置并不是安全边界。服务端代码仍可能错误地把私密值返回:

// 错误示例
export default defineEventHandler(() => {
  return useRuntimeConfig()
})

应只返回明确允许公开的字段:

export default defineEventHandler((event) => {
  const config = useRuntimeConfig(event)

  return {
    appName: config.public.appName,
  }
})

五、缓存:缓存的不是“函数”,而是带条件的响应

缓存的核心条件是:对于同一个缓存键,后续请求可以接受之前保存的结果。

形式化地说,若响应函数为:

R=F(K,S)R = F(K, S)

其中:

  • KK 是请求键,例如路径、查询参数和必要的请求头;
  • SS 是外部状态,例如数据库内容;
  • RR 是响应。

当缓存只使用 KK 作为键时,必须满足:

F(K,S1)=F(K,S2)F(K, S_1) = F(K, S_2)

或者至少在缓存有效期内,业务允许把不同状态下的结果视为相同。若响应还依赖用户身份 U,则缓存键必须包含用户维度:

K=(K,U)K' = (K, U)

否则用户 A 的响应可能被用户 B 读取。这是服务端缓存最严重的错误之一。

1. 路由级缓存规则

可以在 nuxt.config.ts 中声明路由规则:

export default defineNuxtConfig({
  routeRules: {
    '/blog/**': {
      swr: 3600,
    },
    '/api/health': {
      cache: {
        maxAge: 10,
      },
    },
  },
})

这里的语义需要区分:

  • swr: 3600 表示允许使用 stale-while-revalidate 语义,具体实现由 Nitro 和部署平台决定;
  • cache.maxAge: 10 表示缓存有效时间为 10 秒;
  • 不同 Nitro/Nuxt 版本对路由规则、平台适配和缓存后端的支持可能不同;
  • 路由规则不是数据库级缓存,也不是自动生成 CDN 缓存控制策略的绝对保证。

静态博客页面通常可以容忍短时间旧数据,而用户账户、购物车、权限和支付状态通常不能使用共享缓存。尤其要避免对带有 Cookie、Authorization 或用户私有数据的接口随意配置公共缓存。

2. Handler 级缓存

对于可以缓存的服务端处理器,可以使用 Nitro 提供的缓存 Handler 能力。具体导入方式和可用选项可能随 Nitro 版本变化,典型形式如下:

// server/api/catalog.get.ts
export default defineCachedEventHandler(async () => {
  const response = await fetch('https://example.com/catalog.json')

  if (!response.ok) {
    throw createError({
      statusCode: 502,
      statusMessage: 'Catalog upstream failed',
    })
  }

  return response.json()
}, {
  maxAge: 300,
})

这段代码的前提是:目录数据允许在 300 秒内被多个请求共享。若上游请求失败,不能把失败结果误当成成功数据缓存;同时还需要考虑上游超时、缓存击穿和缓存失效后的并发请求。

如果响应依赖查询参数,必须确认缓存键会区分不同 URL。例如:

/api/search?q=nuxt
/api/search?q=vue

不能共享同一个没有查询参数区分的缓存键。对于依赖认证信息的接口,除非明确设计了按用户隔离的缓存,否则应关闭共享缓存。

3. 缓存失效比缓存写入更难

假设商品列表缓存为:

C=F(S)C = F(S)

管理员更新数据库状态 SS 后,如果没有失效动作,缓存仍是:

Cold=F(Sold)C_{\text{old}} = F(S_{\text{old}})

此时数据库已经是新状态,但客户端继续得到旧结果。常见处理方式有:

  • 缩短 maxAge,接受最终一致性;
  • 更新数据后删除对应缓存;
  • 使用版本化缓存键,例如 catalog:v2
  • 对必须即时一致的接口不使用共享缓存。

不同部署平台的缓存存储和失效 API 并不完全一致,不能假设本地开发环境清空内存缓存的方式在 Serverless 或边缘环境同样有效。

六、SSR、预渲染、ISR 与 API 缓存的区别

Nuxt 中至少存在三种“结果被复用”的场景:

1. SSR

SSR 是请求到达时在服务端生成 HTML:

HTML=Render(Page,Request)HTML = Render(Page, Request)

每次请求都可能重新执行页面数据获取和组件渲染。SSR 不等于页面被缓存,也不等于 API 响应被缓存。

2. 预渲染

预渲染在构建阶段生成页面文件。构建时无法访问生产环境才存在的数据库状态,适合文档、营销页和内容快照。

如果项目使用纯静态输出,例如:

npx nuxt generate

则输出主要是静态文件。此时不能把 server/api 当成一个持续运行的 Node API 服务来使用;部署平台只会提供生成出来的静态资源,除非另行部署 API 后端或使用平台函数能力。

3. ISR/SWR

ISR 或 SWR 是在预生成或首次请求后,在一段时间内复用页面结果,并在过期后重新生成。它解决的是页面或路由响应的更新频率问题,不等同于数据库缓存,也不等同于浏览器缓存。

因此以下三个问题必须分开回答:

  1. 页面 HTML 是否由服务端生成?
  2. API Handler 是否缓存响应?
  3. 浏览器或 CDN 是否缓存 HTTP 响应?

修改 server/api/users.get.ts 的缓存规则,不会自动改变页面 HTML 的 SSR 行为;修改页面的 swr 规则,也不代表页面内部调用的所有 API 都获得相同缓存策略。

七、一个完整的 API 示例:配置、校验、错误和客户端调用

下面实现一个读取公开配置、接收 POST 数据并返回明确错误的接口。

// server/api/profile.post.ts
import { z } from 'zod'

const profileSchema = z.object({
  displayName: z.string().trim().min(2).max(40),
})

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig(event)
  const body = await readBody<unknown>(event)

  const result = profileSchema.safeParse(body)

  if (!result.success) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Invalid request body',
      data: {
        code: 'INVALID_BODY',
      },
    })
  }

  const upstream = await $fetch<{ id: string }>(
    `${config.public.apiBase}/profiles`,
    {
      method: 'POST',
      body: result.data,
      headers: {
        'x-service-token': config.apiSecret,
      },
    },
  )

  return {
    id: upstream.id,
    displayName: result.data.displayName,
  }
})

前置条件:

npm install zod

这个处理器的执行顺序是:

  1. useRuntimeConfig(event) 读取服务端配置;
  2. readBody 读取未知类型的请求体;
  3. 用 schema 验证,而不是把客户端输入直接断言成可信类型;
  4. 验证失败时返回 400
  5. 使用私有的 apiSecret 调用上游服务;
  6. 只返回业务允许公开的结果。

客户端调用:

await $fetch('/api/profile', {
  method: 'POST',
  body: {
    displayName: 'Ada Lovelace',
  },
})

请求体不符合约束时,服务端应返回 HTTP 400。上游失败时,$fetch 会抛出异常,Nuxt 会将其转换为错误响应;生产代码还可以捕获异常并映射为不泄露上游细节的 502 Bad Gateway

不要把 TypeScript 类型断言当成运行时校验:

const body = await readBody<{
  displayName: string
}>(event)

这只告诉 TypeScript“假设它是这个类型”,不会检查客户端是否真的发送了字符串。网络边界必须使用运行时校验。

八、部署:构建产物、Node、Serverless 和静态输出

1. Node 部署

普通 Node 部署通常执行:

npm run build
node .output/server/index.mjs

构建成功后,Nuxt/Nitro 会生成 .output 目录。index.mjs 是 Node 服务器入口,实际端口通常由 PORT 环境变量控制:

PORT=8080 node .output/server/index.mjs

验证:

curl -i http://127.0.0.1:8080/api/health

应看到类似:

HTTP/1.1 200 OK
content-type: application/json

并得到健康检查 JSON。

生产部署时需要验证:

  • 构建阶段是否成功;
  • .output 是否被完整复制到运行镜像;
  • PORT 是否与平台分配的端口一致;
  • NUXT_... 环境变量是否在运行阶段存在;
  • 反向代理是否正确转发 Host、协议和客户端 IP;
  • 健康检查是否访问一个无数据库依赖或依赖明确的接口。

2. Serverless 和边缘运行时

Nitro 支持不同部署预设。可以通过环境变量选择预设,具体名称取决于当前 Nitro 版本和目标平台:

NITRO_PRESET=<target> npm run build

不同运行时的约束并不相同:

  • Node 通常允许长期进程、连接池和本地临时文件;
  • Serverless 实例可能冷启动、随时销毁,并发请求不一定落到同一实例;
  • 边缘运行时可能不支持完整 Node API、原生模块或传统 TCP 数据库连接;
  • 本地内存缓存可能只在单个实例内有效;
  • 多实例之间不能依赖模块级变量同步状态。

因此,下面这种“进程内计数器”不能作为生产全局计数:

let count = 0

export default defineEventHandler(() => {
  count += 1
  return { count }
})

在单个本地进程中它会递增;部署到多实例环境后,每个实例都有自己的 count,重启后也会归零。需要一致计数时,应使用 Redis、数据库或平台提供的共享存储。

3. 静态输出的边界

如果部署目标只有静态文件服务器:

npx nuxt generate

则服务端路由不会作为一个常驻 Nitro HTTP 服务运行。以下设计会失败:

  • 浏览器请求 /api/orders,期待 Nuxt 在静态服务器上访问数据库;
  • server/api 中读取仅存在于服务器环境的密钥;
  • 依赖每次请求执行的认证中间件;
  • 依赖服务端缓存和实时数据库状态。

静态站点仍可以调用外部 API,但该 API 必须由另一个持续运行的后端、Serverless Function 或边缘函数提供。

九、生产故障的诊断路径

1. 页面正常,API 404

先确认路径前缀:

server/api/foo.get.ts    -> /api/foo
server/routes/foo.get.ts -> /foo

然后确认构建产物是否包含该路由,以及部署方式是否是静态输出。如果使用 nuxt generate,服务端 API 没有常驻进程是最常见原因之一。

2. 本地有值,生产配置为空

检查三个层面:

printenv | grep '^NUXT_'

然后确认:

  • 环境变量是否注入到运行容器,而不是只注入构建容器;
  • 名称是否与 runtimeConfig 层级匹配;
  • 私有字段是否误放到 public
  • 是否在服务端使用了 useRuntimeConfig(event)
  • 平台是否在启动命令执行前设置变量。

如果配置只在构建阶段存在,而运行时容器没有该变量,使用运行时配置的代码可能在生产请求中得到空值或默认值。

3. 用户看到别人的数据

优先检查缓存,而不是先怀疑 Vue 响应式系统:

  • 是否给带 Cookie 或 Authorization 的接口设置了共享缓存;
  • 缓存键是否忽略了查询参数;
  • 是否缓存了用户私有页面;
  • CDN 是否缓存了没有正确 Cache-Control 的响应;
  • 服务端是否把用户状态放进了模块级变量。

如果响应依赖用户身份,应默认视为不可共享,除非已经设计并验证了按用户隔离的缓存键和失效策略。

4. 本地正常,Serverless 超时

常见原因包括:

  • 在每个请求中重复建立数据库连接;
  • 依赖本地文件持久化;
  • 使用了不受目标运行时支持的 Node API;
  • 上游请求没有超时;
  • 把冷启动和网络延迟误判为缓存问题。

应先记录请求总耗时、上游耗时和数据库耗时,再决定是连接管理、运行时兼容性还是外部服务故障。

十、几个容易混淆的结论

Server Routes 不等于 API Routes。 server/api 是 Server Routes 的一个约定目录;server/routes、动态路由和服务端中间件也属于 Nitro 服务端能力。

Nitro 不等于数据库。 Nitro 负责服务端运行时和部署适配,不会自动提供数据持久化、事务或跨实例状态。

运行时配置不等于机密管理系统。 它可以避免把值硬编码进业务代码,但密钥仍应由容器密钥、云平台 Secret 或专门的密钥系统注入,并且不能进入 public 或响应正文。

SSR 不等于缓存。 SSR 描述生成 HTML 的位置;缓存描述结果是否被复用。两者可以独立配置。

TypeScript 类型不等于输入验证。 编译器无法验证真实 HTTP 请求体,边界数据仍需要运行时 schema 校验。

本地成功不等于部署可用。 本地通常是单进程、长生命周期、完整 Node 环境;生产可能是多实例、Serverless 或边缘运行时。缓存、连接、文件和全局变量的语义都会因此改变。

Nuxt Server Routes 的核心不是文件名本身,而是把“请求如何进入系统、由哪个 Handler 处理、读取哪些运行时配置、响应是否可缓存、最终在哪种运行时执行”完整连接起来。只有沿着这条因果链检查,才能正确判断一个接口是路由问题、配置问题、缓存问题,还是部署模型本身不匹配。


系列导航与关联阅读

官方资料

本文依据 Vue、Vite 与生态项目官方文档重新梳理;正文与示例由 WR BLOG 编写。