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 目录中。

src/app/hello+api.ts
export function GET(request: Request) { return Response.json({ hello: 'world' }); }

服务器功能需要自定义服务器,它可以部署到 EAS 或大多数其他托管提供商

观看:Expo Router API 路由如何处理请求并流式传输数据
观看:Expo Router API 路由如何处理请求并流式传输数据

使用 Expo Router API 路由创建服务器端点,以处理请求、返回 JSON 并流式传输数据。

什么是 API 路由

API 路由是在路由匹配时于服务器上执行的函数。它们可用于安全地处理敏感数据,例如 API 密钥,或实现自定义服务器逻辑,例如交换认证代码以获取访问令牌。API 路由应在兼容 WinterCG 的环境中执行。

在 Expo 中,API 路由通过在 app 目录中创建带有 +api.ts 扩展名的文件来定义。例如,当路由 /hello 匹配时,会执行下面这个 API 路由。

src
app
  index.tsx
  hello+api.tsAPI 路由

创建一个 API 路由

1

确保你的项目使用的是服务器输出,这将配置导出和生产构建同时生成服务器包和客户端包。

app.json
{ "web": { "output": "server" } }

2

app 目录中创建一个 API 路由。例如,添加以下路由处理器。当路由 /hello 匹配时,它会被执行。

src/app/hello+api.ts
export function GET(request: Request) { return Response.json({ hello: 'world' }); }

你可以从服务器路由中导出以下任意函数:GETPOSTPUTPATCHDELETEHEADOPTIONS。当匹配到对应的 HTTP 方法时,该函数会执行。不支持的方法将自动返回 405: Method not allowed

3

使用 Expo CLI 启动开发服务器:

Terminal
npx expo

4

你可以向该路由发起网络请求以访问数据。运行以下命令测试该路由:

Terminal
curl http://localhost:8081/hello

你也可以从客户端代码中发起请求:

src/app/index.tsx
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 字段在生产环境中进行配置:

app.json
{ "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 对象。

src/app/blog/[post]+api.ts
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() 函数来访问请求体。它会自动解析请求体并返回结果。

src/app/validate+api.ts
export async function POST(request: Request) { const body = await request.json(); return Response.json({ ... }); }

请求查询参数

可以通过解析请求 URL 来访问查询参数:

src/app/endpoint+api.ts
export async function GET(request: Request) { const url = new URL(request.url); const post = url.searchParams.get('post'); // 获取 'post' 的数据 return Response.json({ ... }); }

响应

响应使用全局标准的 Response 对象。

src/app/demo+api.ts
export function GET() { return Response.json({ hello: 'universe' }); }

错误

对于错误情况,你可以创建任意状态码和响应体的 Response

src/app/blog/[post]+api.ts
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 代码中都有效的工具和代码模式。这包括获取请求元数据、调度任务以及错误处理的工具。

Terminal
npx expo install expo-server

使用 expo-server 不仅限于 API 路由,它也可以用于任何其他服务器代码,例如服务器中间件

错误处理

你可以通过抛出 StatusError 来中止请求,并改为返回一个错误 Response。这是一个特殊的 Error 实例,它会被替换为一个 HTTP 响应,从而取代错误本身。

src/app/blog/[post]+api.ts
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 包装。例如,这可用于创建重定向响应。

src/app/blog/[post]+api.ts
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-serverorigin() 辅助方法来访问此值。

src/app/help+api.ts
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-serverenvironment() 辅助方法来获取环境名称。该值会因你运行服务器代码的方式不同而不同。

src/app/env+api.ts
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 }); } }

任务调度

在请求处理器中,你可能需要并行执行一些异步任务,以配合你的服务器逻辑。

src/app/tasks+api.ts
export async function GET(request: Request) { // 这会延迟响应: await pingAnalytics(...); const data = await fetchExampleData(...); return Response.json({ data }); }

在上面的示例中,一个 await 的函数调用会延迟 API 路由其余部分的执行。如果我们不想延迟 Response,那么 await 这个调用就不合适。不过,如果不使用 await 调用函数,也无法保证该任务会让无服务器函数持续运行。

相反,你可以使用 expo-serverrunTask() 辅助函数来运行并发任务。这相当于你在 service worker 代码或其他无服务器运行时中看到的 waitUntil() 方法。

src/app/tasks+api.ts
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-serverdeferTask() 辅助函数来安排任务在 API 路由解析出 Response 之后再运行。

src/app/tasks+api.ts
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 添加元数据。

src/app/+middleware.ts
import { setResponseHeaders } from 'expo-server'; export default function middleware(request: Request) { // 限流器通常会添加 `Retry-After` header setResponseHeaders({ 'Retry-After': '3600' }); }

在上面的示例中,Retry-After header 会被添加到未来某个 API 路由可能创建的 Response 中。这同样也可以扩展用于认证和 cookies。

src/app/+middleware.ts
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.jsmetro.config.js 支持 — 配置会同时作用于客户端和服务端代码。

安全性

路由处理程序在一个与客户端代码隔离的沙箱环境中执行。这意味着你可以安全地将敏感数据存储在路由处理程序中,而不会暴露给客户端。

  • 导入了包含密钥的代码的客户端代码会被包含在客户端 bundle 中。它适用于 src/app 目录中的所有文件,即使它们不是路由处理程序文件(例如后缀为 +api.ts 的文件)。
  • 如果密钥位于 <...>+api.ts 文件中,它不会被包含在客户端 bundle 中。它适用于所有被路由处理程序导入的文件。
  • 密钥剥离在 expo/metro-config 中进行,并且需要在 metro.config.js 中使用它。

部署

当你准备将其部署到生产环境时,运行以下命令,在 dist 目录中创建 server bundle(更多细节请参阅 Expo CLI 文档):

Terminal
npx expo export --platform web

你可以使用 npx expo serve 在本地测试该服务器,在网页浏览器中访问该 URL,或者使用 origin 设置为本地服务器 URL 来创建原生构建。 你可以使用 EAS Hosting 或其他第三方服务将该服务器部署到生产环境。

如果你想导出 API 路由并跳过生成应用的网站版本,可以使用以下命令,它将生成一个仅包含项目 server 代码的 dist 目录。

Terminal
npx expo export --platform web --no-ssg
使用 EAS 立即部署

EAS Hosting 是部署你的 Expo API 路由和服务器的最佳方式。

原生部署

重要 这是一个 alpha 功能。未来版本中,该流程将更加自动化并获得更好的支持。

Expo Router 中的服务器功能(API 路由和 React Server Components)基于 window.locationfetch 的原生实现,它们指向远程服务器。在开发环境中,我们会自动将其指向由 npx expo start 运行的开发服务器,但要让生产环境的原生构建正常工作,你需要将服务器部署到安全的主机,并设置 Expo Router Config Plugin 的 origin 属性。

配置完成后,像相对 fetch 请求 fetch('/my-endpoint') 这样的功能会自动指向服务器 origin。

可以使用环境变量 EXPO_UNSTABLE_DEPLOY_SERVER=1 对此部署流程进行实验性自动化,以确保原生构建期间的版本正确。

以下是如何配置原生应用,以便在构建时自动部署并链接一个带版本的生产服务器:

1

确保 app.jsonexpo.extra.router.origin 字段中没有设置 origin。另外,确保你没有使用 app.config.js,因为目前自动链接的部署还不支持它。

2

通过先在本地部署一次来为项目设置 EAS Hosting

Terminal
npx expo export -p web
eas deploy

3

在你的 .env 文件中设置 EXPO_UNSTABLE_DEPLOY_SERVER 环境变量。它将用于在 EAS Build 期间启用实验性的服务器部署功能。

.env
EXPO_UNSTABLE_DEPLOY_SERVER=1

4

你现在可以使用自动服务器部署了!运行构建命令以开始该流程。

Terminal
eas build

你也可以在本地这样运行:

Terminal
# Android
npx expo run:android --variant release

# iOS
npx expo run:ios --configuration Release

关于原生应用自动服务器部署的说明:

  • 如果某些内容未正确设置,服务器失败可能会发生在 EAS Build 的 Bundle JavaScript 阶段。
  • 如果你愿意,也可以在构建应用之前手动部署服务器并设置 origin URL。
  • 可以使用环境变量 EXPO_NO_DEPLOY=1 强制跳过自动部署。
  • 自动部署目前还不支持 动态应用配置app.config.jsapp.config.ts)文件。
  • 部署日志将写入 .expo/logs/deploy.log
  • EXPO_OFFLINE 模式下不会运行部署。

在本地测试原生生产应用

通常,将生产构建与本地开发服务器一起测试会很有用,以确保一切按预期工作。这可以显著加快调试过程。

1

导出生产服务器:

Terminal
npx expo export

2

在本地托管生产服务器:

Terminal
npx expo serve

3

app.jsonorigin 字段中设置 origin。确保 expo.extra.router.origin 中没有生成的值。该值应为 http://localhost:8081(假设 npx expo serve 运行在默认端口上)。

app.json
{ "expo": { "plugins": [ [ "expo-router", { "origin": "http://localhost:8081" } ] ] } }

请记得在部署到生产环境时移除这个 origin 值。

4

在模拟器上以 release 模式构建应用:

Terminal
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 库。

Terminal
npx expo install expo-server

Bun

1

导出用于生产环境的网站:

Terminal
bunx expo export -p web

2

编写一个服务器入口文件,用于提供静态文件并将请求委派给服务器路由:

server.ts
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 启动服务器:

Terminal
bun run server.ts

Express

1

安装所需依赖:

Terminal
npm i -D express compression morgan

2

导出用于生产环境的网站:

Terminal
npx expo export -p web

3

编写一个服务器入口文件,用于提供静态文件并将请求委派给服务器路由:

server.ts
#!/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 命令启动服务器:

Terminal
node server.ts

Netlify

重要 第三方适配器可能会发生破坏性变更。我们没有对它们进行持续测试。

1

创建一个服务器入口文件。所有请求都将通过该中间件进行委派。文件的具体位置很重要。

netlify/functions/server.ts
import path from 'node:path'; import { createRequestHandler } from 'expo-server/adapter/netlify'; export default createRequestHandler({ build: path.join(__dirname, '../../dist/server'), });

2

在项目根目录创建一个 Netlify 配置文件,将所有请求重定向到服务器函数。

netlify.toml
[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 构建网站和函数:

Terminal
npx expo export -p web

4

使用 Netlify CLI 部署到 Netlify。

Terminal
# 如有需要,先全局安装 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

创建一个服务器入口文件。所有请求都将通过该中间件进行委派。文件的具体位置很重要。

api/index.ts
const { createRequestHandler } = require('expo-server/adapter/vercel'); module.exports = createRequestHandler({ build: require('path').join(__dirname, '../dist/server'), });

2

在项目根目录创建一个 Vercel 配置文件(vercel.json),将所有请求重定向到服务器函数。

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 不再使用 routesbuilds 配置选项,并且会自动从 dist/client 输出目录提供你的公共资源。

vercel.json
{ "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。

Terminal
# 如有需要,先全局安装 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。这将在未来版本中得到解决。