API 路由
编辑页面
了解如何使用 Expo Router 创建服务器端点。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
Expo Router 让你能够为所有平台编写安全的服务器代码,就在你的 src/app 目录中。
export function GET(request: Request) { return Response.json({ hello: 'world' }); }
服务器功能需要自定义服务器,它可以部署到 EAS 或大多数其他托管提供商。

使用 Expo Router API 路由创建服务器端点,以处理请求、返回 JSON 并流式传输数据。
什么是 API 路由
API 路由是在路由匹配时于服务器上执行的函数。它们可用于安全地处理敏感数据,例如 API 密钥,或实现自定义服务器逻辑,例如交换认证代码以获取访问令牌。API 路由应在兼容 WinterCG 的环境中执行。
在 Expo 中,API 路由通过在 app 目录中创建带有 +api.ts 扩展名的文件来定义。例如,当路由 /hello 匹配时,会执行下面这个 API 路由。
srcappindex.tsxhello+api.tsAPI 路由创建一个 API 路由
1
确保你的项目使用的是服务器输出,这将配置导出和生产构建同时生成服务器包和客户端包。
{ "web": { "output": "server" } }
2
在 app 目录中创建一个 API 路由。例如,添加以下路由处理器。当路由 /hello 匹配时,它会被执行。
export function GET(request: Request) { return Response.json({ hello: 'world' }); }
你可以从服务器路由中导出以下任意函数:GET、POST、PUT、PATCH、DELETE、HEAD 和 OPTIONS。当匹配到对应的 HTTP 方法时,该函数会执行。不支持的方法将自动返回 405: Method not allowed。
3
使用 Expo CLI 启动开发服务器:
- npx expo4
你可以向该路由发起网络请求以访问数据。运行以下命令测试该路由:
- curl http://localhost:8081/hello你也可以从客户端代码中发起请求:
import { Button } from 'react-native'; async function fetchHello() { const response = await fetch('/hello'); const data = await response.json(); alert('Hello ' + data.hello); } export default function App() { return <Button onPress={() => fetchHello()} title="获取 hello" />; }
相对 fetch 请求在开发环境中会自动相对于开发服务器源地址进行请求,并且可以在 app.json 中通过 origin 字段在生产环境中进行配置:
{ "plugins": [ [ "expo-router", { "origin": "https://evanbacon.dev/" } ] ] }
通过设置环境变量 EXPO_UNSTABLE_DEPLOY_SERVER=1,可以在 EAS Build 期间自动配置此 URL。这将触发一个版本化的服务器部署,并自动将 origin 设置为预览部署 URL。
5
将网站和服务器部署到托管提供商,即可在原生端和 web 端的生产环境中访问这些路由。
API 路由文件名不能带有平台特定扩展名。例如,hello+api.web.ts 将无法工作。
请求
请求使用全局标准的 Request 对象。
export async function GET(request: Request, { post }: Record<string, string>) { // const postId = new URL(request.url).searchParams.get('post') // 获取 'post' 的数据 return Response.json({ ... }); }
请求体
使用 request.json() 函数来访问请求体。它会自动解析请求体并返回结果。
export async function POST(request: Request) { const body = await request.json(); return Response.json({ ... }); }
请求查询参数
可以通过解析请求 URL 来访问查询参数:
export async function GET(request: Request) { const url = new URL(request.url); const post = url.searchParams.get('post'); // 获取 'post' 的数据 return Response.json({ ... }); }
响应
响应使用全局标准的 Response 对象。
export function GET() { return Response.json({ hello: 'universe' }); }
错误
对于错误情况,你可以创建任意状态码和响应体的 Response。
export async function GET(request: Request, { post }: Record<string, string>) { if (!post) { return new Response('未找到帖子', { status: 404, headers: { 'Content-Type': 'text/plain', }, }); } // 获取 `post` 的数据 return Response.json({ ... }); }
使用未定义的方法发起请求时,将自动返回 405: Method not allowed。如果请求过程中抛出错误,将自动返回 500: Internal server error。
运行时 API
服务器运行时 API 和expo-server在 SDK 54 及更高版本中可用,并且在生产环境使用时需要已部署的服务器。
你可以使用 expo-server 库来使用多种在任何服务端 Expo 代码中都有效的工具和代码模式。这包括获取请求元数据、调度任务以及错误处理的工具。
- npx expo install expo-server使用 expo-server 不仅限于 API 路由,它也可以用于任何其他服务器代码,例如服务器中间件。
错误处理
你可以通过抛出 StatusError 来中止请求,并改为返回一个错误 Response。这是一个特殊的 Error 实例,它会被替换为一个 HTTP 响应,从而取代错误本身。
import { StatusError } from 'expo-server'; export async function GET(request: Request, { post }: Record<string, string>) { if (!post) { throw new StatusError(404, '未找到帖子'); } // ... }
在编写自己的服务器工具和辅助函数时,StatusError 是处理异常的一种更方便的方式,因为抛出它们会中断任何 API 函数并提前返回错误。
StatusError 接受状态码和错误消息,这些内容也可以选择以 JSON 或 Error 对象形式传入,并且始终会返回一个带有 JSON 响应体的 Response,其中 error 键被设置为它们的错误消息。
这可能会有一定限制,并不适用于所有情况。有时,改为 throw 一个 Response 对象可能更有益,这同样会中断你的逻辑,但会直接用 API 路由的解析结果 Response 替换,而不会使用 StatusError 包装。例如,这可用于创建重定向响应。
import { StatusError } from 'expo-server'; export async function GET(request: Request, { post }: Record<string, string>) { if (!post) { throw Response.redirect('https://expo.dev', 302); } // ... }
请求元数据
请求通常会在其 headers 中携带大多数你所需的元数据。不过,expo-server 提供了一些辅助函数,可以更轻松地获取常见值。
expo-server 中的辅助函数返回的值都限定于当前 Request。你只能在服务端代码中、且仅在请求进行期间调用这些函数。
你可能需要访问的一个常见值是请求的 origin URL。origin URL 通常通过请求的 Origin header 传递,表示用户用于访问你的 API 路由的 URL。这可能与服务器在请求被代理时所看到的内部部署 URL 不同。你可以使用 expo-server 的 origin() 辅助方法来访问此值。
import { origin } from 'expo-server'; export async function GET(request: Request) { const target = new URL('/help', origin() ?? request.url); return Response.redirect('https://expo.dev', 302); }
你部署服务器代码的大多数运行时都有“环境”的概念,用来区分生产或预发布部署。你可以使用 expo-server 的 environment() 辅助方法来获取环境名称。该值会因你运行服务器代码的方式不同而不同。
import { environment } from 'expo-server'; export async function GET(request: Request) { const env = environment(); if (env === 'staging') { return Response.json({ isStaging: true }); } else if (!env) { return Response.json({ isProduction: true }); } else { return Response.json({ env }); } }
任务调度
在请求处理器中,你可能需要并行执行一些异步任务,以配合你的服务器逻辑。
export async function GET(request: Request) { // 这会延迟响应: await pingAnalytics(...); const data = await fetchExampleData(...); return Response.json({ data }); }
在上面的示例中,一个 await 的函数调用会延迟 API 路由其余部分的执行。如果我们不想延迟 Response,那么 await 这个调用就不合适。不过,如果不使用 await 调用函数,也无法保证该任务会让无服务器函数持续运行。
相反,你可以使用 expo-server 的 runTask() 辅助函数来运行并发任务。这相当于你在 service worker 代码或其他无服务器运行时中看到的 waitUntil() 方法。
import { runTask } from 'expo-server'; export async function GET(request: Request) { // 这不会延迟响应: runTask(async () => { await pingAnalytics(...); }); const data = await fetchExampleData(...); return Response.json({ data }); }
使用 runTask 时,你可以在 await 和不 await 异步函数之间取得平衡。它们会并发运行,不会延迟 API 路由的响应或执行,但也会确保运行时知道这些任务的存在,并且不会过早退出。
不过,有时你可能希望把任务延后到 API 路由返回 Response 之后再执行。在这种情况下,如果 API 已经拒绝该任务,你可能会希望不要执行它。另外,你也可能希望只在某个时间敏感任务完成后再运行一个函数,以防并发代码延迟你 API 路由中计算密集型任务的执行。
你可以使用 expo-server 的 deferTask() 辅助函数来安排任务在 API 路由解析出 Response 之后再运行。
import { deferTask } from 'expo-server'; export async function GET(request: Request) { // 这会在整个函数解析完成后运行: deferTask(async () => { await pingAnalytics(...); }); const data = await fetchExampleData(...); return Response.json({ data }); }
响应头
当将服务器逻辑拆分并组织到多个辅助函数和文件中时,可能需要在 Response 创建之前修改其 headers。
例如,你可能需要在服务器中间件中,在 API 路由代码运行之前向 Response 添加元数据。
import { setResponseHeaders } from 'expo-server'; export default function middleware(request: Request) { // 限流器通常会添加 `Retry-After` header setResponseHeaders({ 'Retry-After': '3600' }); }
在上面的示例中,Retry-After header 会被添加到未来某个 API 路由可能创建的 Response 中。这同样也可以扩展用于认证和 cookies。
import { setResponseHeaders } from 'expo-server'; export default function middleware(request: Request) { // 向未来的响应追加 cookie setResponseHeaders(headers => { headers.append('Set-Cookie', 'token=123; Secure'); }); }
打包
API 路由会与 Expo CLI 和 Metro bundler 一起打包。它们可以使用与你的客户端代码相同的所有语言特性:
- TypeScript — 类型以及 tsconfig.json 路径。
- 环境变量 — 服务端路由可以访问所有环境变量,而不只是那些以前缀
EXPO_PUBLIC_开头的变量。 - Node.js 标准库 — 确保你在本地为服务器环境使用了正确版本的 Node.js。
- babel.config.js 和 metro.config.js 支持 — 配置会同时作用于客户端和服务端代码。
安全性
路由处理程序在一个与客户端代码隔离的沙箱环境中执行。这意味着你可以安全地将敏感数据存储在路由处理程序中,而不会暴露给客户端。
- 导入了包含密钥的代码的客户端代码会被包含在客户端 bundle 中。它适用于 src/app 目录中的所有文件,即使它们不是路由处理程序文件(例如后缀为 +api.ts 的文件)。
- 如果密钥位于 <...>+api.ts 文件中,它不会被包含在客户端 bundle 中。它适用于所有被路由处理程序导入的文件。
- 密钥剥离在
expo/metro-config中进行,并且需要在 metro.config.js 中使用它。
部署
当你准备将其部署到生产环境时,运行以下命令,在 dist 目录中创建 server bundle(更多细节请参阅 Expo CLI 文档):
- npx expo export --platform web你可以使用 npx expo serve 在本地测试该服务器,在网页浏览器中访问该 URL,或者使用 origin 设置为本地服务器 URL 来创建原生构建。
你可以使用 EAS Hosting 或其他第三方服务将该服务器部署到生产环境。
如果你想导出 API 路由并跳过生成应用的网站版本,可以使用以下命令,它将生成一个仅包含项目 server 代码的 dist 目录。
- npx expo export --platform web --no-ssgEAS Hosting 是部署你的 Expo API 路由和服务器的最佳方式。
原生部署
重要 这是一个 alpha 功能。未来版本中,该流程将更加自动化并获得更好的支持。
Expo Router 中的服务器功能(API 路由和 React Server Components)基于 window.location 和 fetch 的原生实现,它们指向远程服务器。在开发环境中,我们会自动将其指向由 npx expo start 运行的开发服务器,但要让生产环境的原生构建正常工作,你需要将服务器部署到安全的主机,并设置 Expo Router Config Plugin 的 origin 属性。
配置完成后,像相对 fetch 请求 fetch('/my-endpoint') 这样的功能会自动指向服务器 origin。
可以使用环境变量 EXPO_UNSTABLE_DEPLOY_SERVER=1 对此部署流程进行实验性自动化,以确保原生构建期间的版本正确。
以下是如何配置原生应用,以便在构建时自动部署并链接一个带版本的生产服务器:
1
确保 app.json 或 expo.extra.router.origin 字段中没有设置 origin。另外,确保你没有使用 app.config.js,因为目前自动链接的部署还不支持它。
2
通过先在本地部署一次来为项目设置 EAS Hosting。
- npx expo export -p web- eas deploy3
在你的 .env 文件中设置 EXPO_UNSTABLE_DEPLOY_SERVER 环境变量。它将用于在 EAS Build 期间启用实验性的服务器部署功能。
EXPO_UNSTABLE_DEPLOY_SERVER=1
4
你现在可以使用自动服务器部署了!运行构建命令以开始该流程。
- eas build你也可以在本地这样运行:
# Android- npx expo run:android --variant release# iOS- npx expo run:ios --configuration Release关于原生应用自动服务器部署的说明:
- 如果某些内容未正确设置,服务器失败可能会发生在 EAS Build 的
Bundle JavaScript阶段。 - 如果你愿意,也可以在构建应用之前手动部署服务器并设置
originURL。 - 可以使用环境变量
EXPO_NO_DEPLOY=1强制跳过自动部署。 - 自动部署目前还不支持 动态应用配置(app.config.js 和 app.config.ts)文件。
- 部署日志将写入
.expo/logs/deploy.log。 - 在
EXPO_OFFLINE模式下不会运行部署。
在本地测试原生生产应用
通常,将生产构建与本地开发服务器一起测试会很有用,以确保一切按预期工作。这可以显著加快调试过程。
1
导出生产服务器:
- npx expo export2
在本地托管生产服务器:
- npx expo serve3
在 app.json 的 origin 字段中设置 origin。确保 expo.extra.router.origin 中没有生成的值。该值应为 http://localhost:8081(假设 npx expo serve 运行在默认端口上)。
{ "expo": { "plugins": [ [ "expo-router", { "origin": "http://localhost:8081" } ] ] } }
请记得在部署到生产环境时移除这个 origin 值。
4
在模拟器上以 release 模式构建应用:
- EXPO_NO_DEPLOY=1 npx expo run:ios --configuration Release你现在应该能看到请求进入本地服务器。使用像 Proxyman 这样的工具来检查模拟器的网络流量,以获得更深入的了解。
你可以通过 --unstable-rebundle 标志实验性地更改 URL,并在 iOS 上快速重新构建。这会替换 app.json 和客户端资源为新的内容,同时跳过原生重建。
例如,你可以运行 eas deploy 来获取一个新的部署 URL,将其添加到 app.json 中,然后运行 npx expo run:ios --unstable-rebundle --configuration Release,以使用新 URL 快速重新构建应用。
在提交到商店之前,你会希望进行一次干净构建,以确保不存在任何临时性问题。
托管到第三方服务
重要
expo-server库是在 SDK 54 中添加的。对于更早的 SDK,请改用@expo/server。
每个云托管提供商都需要一个自定义适配器来支持 Expo 服务器运行时。以下第三方提供商得到了 Expo 团队非官方或实验性的支持。
在部署到这些提供商之前,最好先熟悉 npx expo export 命令的基础知识:
- dist 是 Expo CLI 的默认导出目录。
- public 目录中的文件在导出时会被复制到 dist。
expo-server包是用于导出的 Expo web 和 API 路由制品的服务端运行时。expo-server不会从 .env 文件中注入环境变量。它们预期由托管提供商或用户来加载。- Metro 不包含在服务器中。
expo-server 库包含针对各种提供商和运行时的适配器。在继续阅读以下任何部分之前,请先安装 expo-server 库。
- npx expo install expo-serverBun
1
导出用于生产环境的网站:
- bunx expo export -p web2
编写一个服务器入口文件,用于提供静态文件并将请求委派给服务器路由:
import { createRequestHandler } from 'expo-server/adapter/bun'; const CLIENT_BUILD_DIR = `${process.cwd()}/dist/client`; const SERVER_BUILD_DIR = `${process.cwd()}/dist/server`; const handleRequest = createRequestHandler({ build: SERVER_BUILD_DIR }); const port = process.env.PORT || 3000; Bun.serve({ port: process.env.PORT || 3000, async fetch(req) { const url = new URL(req.url); console.log('请求 URL:', url.pathname); const staticPath = url.pathname === '/' ? '/index.html' : url.pathname; const file = Bun.file(CLIENT_BUILD_DIR + staticPath); if (await file.exists()) return new Response(await file.arrayBuffer()); return handleRequest(req); }, websocket, }); console.log(`Bun server running at http://localhost:${port}`);
4
使用 bun 启动服务器:
- bun run server.tsExpress
1
安装所需依赖:
- npm i -D express compression morgan2
导出用于生产环境的网站:
- npx expo export -p web3
编写一个服务器入口文件,用于提供静态文件并将请求委派给服务器路由:
#!/usr/bin/env node const path = require('path'); const { createRequestHandler } = require('expo-server/adapter/express'); const express = require('express'); const compression = require('compression'); const morgan = require('morgan'); const CLIENT_BUILD_DIR = path.join(process.cwd(), 'dist/client'); const SERVER_BUILD_DIR = path.join(process.cwd(), 'dist/server'); const app = express(); app.use(compression()); // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.disable('x-powered-by'); process.env.NODE_ENV = 'production'; app.use( express.static(CLIENT_BUILD_DIR, { maxAge: '1h', extensions: ['html'], }) ); app.use(morgan('tiny')); app.all( '/{*all}', createRequestHandler({ build: SERVER_BUILD_DIR, }) ); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Express server listening on port ${port}`); });
4
使用 node 命令启动服务器:
- node server.tsNetlify
重要 第三方适配器可能会发生破坏性变更。我们没有对它们进行持续测试。
1
创建一个服务器入口文件。所有请求都将通过该中间件进行委派。文件的具体位置很重要。
import path from 'node:path'; import { createRequestHandler } from 'expo-server/adapter/netlify'; export default createRequestHandler({ build: path.join(__dirname, '../../dist/server'), });
2
在项目根目录创建一个 Netlify 配置文件,将所有请求重定向到服务器函数。
[build] command = "expo export -p web" functions = "netlify/functions" publish = "dist/client" [[redirects]] from = "/*" to = "/.netlify/functions/server" status = 404 [functions] # 包含所有内容,以确保可以使用动态路由。 included_files = ["dist/server/**/*"] [[headers]] for = "/dist/server/_expo/functions/*" [headers.values] # 例如设置为 60 秒。 "Cache-Control" = "public, max-age=60, s-maxage=60"
3
在创建好配置文件后,你可以使用 Expo CLI 构建网站和函数:
- npx expo export -p web4
使用 Netlify CLI 部署到 Netlify。
# 如有需要,先全局安装 Netlify CLI。- npm install netlify-cli -g# 部署网站。- netlify deploy你现在可以通过 Netlify CLI 提供的 URL 访问你的网站。运行 netlify deploy --prod 将发布到生产 URL。
5
如果你正在使用任何环境变量或 .env 文件,请将它们添加到 Netlify。你可以通过进入 Site settings 并将它们添加到 Build & deploy 部分来完成。
Vercel
重要 第三方适配器可能会发生破坏性变更。我们没有对它们进行持续测试。
1
创建一个服务器入口文件。所有请求都将通过该中间件进行委派。文件的具体位置很重要。
const { createRequestHandler } = require('expo-server/adapter/vercel'); module.exports = createRequestHandler({ build: require('path').join(__dirname, '../dist/server'), });
2
在项目根目录创建一个 Vercel 配置文件(vercel.json),将所有请求重定向到服务器函数。
{ "buildCommand": "expo export -p web", "outputDirectory": "dist/client", "functions": { "api/index.ts": { "runtime": "@vercel/node@5.1.8", "includeFiles": "dist/server/**" } }, "rewrites": [ { "source": "/(.*)", "destination": "/api/index" } ] }
更新版本的 vercel.json 不再使用 routes 和 builds 配置选项,并且会自动从 dist/client 输出目录提供你的公共资源。
{ "version": 2, "outputDirectory": "dist", "builds": [ { "src": "package.json", "use": "@vercel/static-build", "config": { "distDir": "dist/client" } }, { "src": "api/index.ts", "use": "@vercel/node", "config": { "includeFiles": ["dist/server/**"] } } ], "routes": [ { "handle": "filesystem" }, { "src": "/(.*)", "dest": "/api/index.ts" } ] }
旧版 的 vercel.json 需要一个 @vercel/static-build 运行时来从 dist/client 输出目录提供你的资源。
3
注意: 此步骤仅适用于 旧版 的 vercel.json 用户。如果你使用的是 v3,可以跳过此步骤。
在创建好配置文件后,在你的 package.json 文件中添加一个 vercel-build 脚本,并将其设置为 expo export -p web。
4
使用 Vercel CLI 部署到 Vercel。
# 如有需要,先全局安装 Vercel CLI。- npm install vercel -g# 构建网站。- vercel build# 部署网站。- vercel deploy --prebuilt你现在可以通过 Vercel CLI 提供的 URL 访问你的网站。
已知限制
API Routes 的 beta 版本目前不支持若干已知功能。
不支持动态导入
API Routes 目前通过将所有代码(不包括 Node.js 内置模块)打包到单个文件中来工作。这意味着你不能使用任何未被服务器打包的外部依赖。例如,像 sharp 这样的库由于包含多个平台的二进制文件,因此无法使用。这将在未来版本中得到解决。
不支持 ESM
当前的打包实现更倾向于统一性,而不是灵活性。这意味着原生不支持 ESM 的限制也延续到了 API Routes。所有代码都会被转译为 Common JS(require/module.exports)。不过,我们仍然建议你使用 ESM 来编写 API Routes。这将在未来版本中得到解决。