mcpField
E
expo-api-routes

expo-api-routes

github.com/sickn33开发49.0k49 次安装今天更新
代码仓库

关于 expo-api-routes

为 Expo 应用开发者提供 API Routes 实践指南,涵盖 `app` 目录中的路由定义、请求处理与部署。重点说明服务端机密、验证和代理等场景,以及 EAS Hosting 的运行限制。

主要功能

  1. 01定义静态与动态 API 路由
  2. 02处理 HTTP 方法、参数与请求体
  3. 03使用环境变量保存服务端机密
  4. 04配置 CORS 与错误处理
  5. 05在本地测试并部署到 EAS Hosting
  6. 06了解 Cloudflare Workers 运行限制

适用场景

  1. 01将第三方 API 密钥保留在服务端
  2. 02接收 webhook 并验证提交数据
  3. 03为移动应用提供服务端 API 端点

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill expo-api-routes

安装后在 Claude Code 中输入/expo-api-routes确认已加载。

附带文件agents/openai.yaml

详细介绍

何时使用 API 路由

在以下情况使用 API 路由:

  • 服务端密钥 — API 密钥、数据库凭据或绝不能到达客户端的令牌
  • 数据库操作 — 不应暴露的直接数据库查询
  • 第三方 API 代理 — 调用外部服务(OpenAI、Stripe 等)时隐藏 API 密钥
  • 服务端验证 — 在写入数据库前验证数据
  • Webhook 端点 — 接收来自 Stripe 或 GitHub 等服务的回调
  • 速率限制 — 在服务端层面控制访问
  • 繁重计算 — 卸载在移动端会很慢的处理任务

何时不使用 API 路由

在以下情况避免使用 API 路由:

  • 数据已公开 — 改为使用直接 fetch 调用公开 API
  • 无需密钥 — 静态数据或客户端安全的操作
  • 需要实时更新 — 使用 WebSocket 或 Supabase Realtime 等服务
  • 简单 CRUD — 考虑使用 Firebase、Supabase 或 Convex 等托管后端
  • 文件上传 — 使用直传存储的上传方式(S3 预签名 URL、Cloudflare R2)
  • 仅需身份验证 — 改为使用 Clerk、Auth0 或 Firebase Auth

文件结构

API 路由位于 app 目录中,带有 +api.ts 后缀:

app/
  api/
    hello+api.ts          → GET /api/hello
    users+api.ts          → /api/users
    users/[id]+api.ts     → /api/users/:id
  (tabs)/
    index.tsx

基本 API 路由

// app/api/hello+api.ts
export function GET(request: Request) {
  return Response.json({ message: "Hello from Expo!" });
}

HTTP 方法

为每个 HTTP 方法导出具名函数:

// app/api/items+api.ts
export function GET(request: Request) {
  return Response.json({ items: [] });
}

export async function POST(request: Request) {
  const body = await request.json();
  return Response.json({ created: body }, { status: 201 });
}

export async function PUT(request: Request) {
  const body = await request.json();
  return Response.json({ updated: body });
}

export async function DELETE(request: Request) {
  return new Response(null, { status: 204 });
}

动态路由

// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
  return Response.json({ userId: id });
}

请求处理

查询参数
export function GET(request: Request) {
  const url = new URL(request.url);
  const page = url.searchParams.get("page") ?? "1";
  const limit = url.searchParams.get("limit") ?? "10";

  return Response.json({ page, limit });
}
请求头
export function GET(request: Request) {
  const auth = request.headers.get("Authorization");

  if (!auth) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  return Response.json({ authenticated: true });
}
JSON 请求体
export async function POST(request: Request) {
  const { email, password } = await request.json();

  if (!email || !password) {
    return Response.json({ error: "Missing fields" }, { status: 400 });
  }

  return Response.json({ success: true });
}

环境变量

使用 process.env 存放服务端密钥:

// app/api/ai+api.ts
export async function POST(request: Request) {
  const { prompt } = await request.json();

  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: "gpt-4",
      messages: [{ role: "user", content: prompt }],
    }),
  });

  const data = await response.json();
  return Response.json(data);
}

设置环境变量:

  • 本地:创建 .env 文件(切勿提交)
  • EAS Hosting:使用 eas env:create 或 Expo 仪表盘

CORS 请求头

为 Web 客户端添加 CORS:

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export function OPTIONS() {
  return new Response(null, { headers: corsHeaders });
}

export function GET() {
  return Response.json({ data: "value" }, { headers: corsHeaders });
}

错误处理

export async function POST(request: Request) {
  try {
    const body = await request.json();
    // Process...
    return Response.json({ success: true });
  } catch (error) {
    console.error("API error:", error);
    return Response.json({ error: "Internal server error" }, { status: 500 });
  }
}

本地测试

启动带有 API 路由的开发服务器:

npx expo serve

这会在 http://localhost:8081 启动一个支持完整 API 路由的本地服务器。

使用 curl 测试:

curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'

部署到 EAS Hosting

前置条件
npm install -g eas-cli
eas login
部署
eas deploy

这会将你的 API 路由构建并部署到 EAS Hosting(Cloudflare Workers)。

生产环境的环境变量
# Create a secret
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production

# Or use the Expo dashboard
自定义域名

在 eas.json 或 Expo 仪表盘中配置。

EAS Hosting 运行时(Cloudflare Workers)

API 路由运行在 Cloudflare Workers 上。主要限制:

缺失/受限的 API
  • 无 Node.js 文件系统 — fs 模块不可用
  • 无原生 Node 模块 — 使用 Web API 或 polyfill
  • 执行时间受限 — CPU 密集型任务有 30 秒超时
  • 无持久连接 — WebSocket 需要 Durable Objects
  • fetch 可用 — 使用标准 fetch 发起 HTTP 请求
改用 Web API
// Use Web Crypto instead of Node crypto
const hash = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode("data")
);

// Use fetch instead of node-fetch
const response = await fetch("https://api.example.com");

// Use Response/Request (already available)
return new Response(JSON.stringify(data), {
  headers: { "Content-Type": "application/json" },
});
数据库选项

由于文件系统不可用,请使用云数据库:

  • Cloudflare D1 — 边缘上的 SQLite
  • Turso — 分布式 SQLite
  • PlanetScale — 无服务器 MySQL
  • Supabase — 带 REST API 的 Postgres
  • Neon — 无服务器 Postgres

使用 Turso 的示例:

// app/api/users+api.ts
import { createClient } from "@libsql/client/web";

const db = createClient({
  url: process.env.TURSO_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

export async function GET() {
  const result = await db.execute("SELECT * FROM users");
  return Response.json(result.rows);
}

从客户端调用 API 路由

// From React Native components
const response = await fetch("/api/hello");
const data = await response.json();

// With body
const response = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John" }),
});

常见模式

身份验证中间件
// utils/auth.ts
export async function requireAuth(request: Request) {
  const token = request.headers.get("Authorization")?.replace("Bearer ", "");

  if (!token) {
    throw new Response(JSON.stringify({ error: "Unauthorized" }), {
      status: 401,
      headers: { "Content-Type": "application/json" },
    });
  }

  // Verify token...
  return { userId: "123" };
}

// app/api/protected+api.ts
import { requireAuth } from "../../utils/auth";

export async function GET(request: Request) {
  const { userId } = await requireAuth(request);
  return Response.json({ userId });
}
代理外部 API
// app/api/weather+api.ts
export async function GET(request: Request) {
  const url = new URL(request.url);
  const city = url.searchParams.get("city");

  const response = await fetch(
    `https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}`
  );

  return Response.json(await response.json());
}

规则

  • 绝不在客户端代码中暴露 API 密钥或密钥
  • 始终验证并清理用户输入
  • 使用恰当的 HTTP 状态码(200、201、400、401、404、500)
  • 使用 try/catch 优雅地处理错误
  • 保持 API 路由聚焦 — 每个端点一个职责
  • 使用 TypeScript 保证类型安全
  • 在服务端记录错误以便调试

局限性

  • 仅当任务明确符合其上游产品或 API 范围时才使用本技能。
  • 在做出更改前,请对照当前官方文档核实命令、API 行为、定价、配额、凭据和部署效果。
  • 不要将生成的示例视为环境特定测试、安全审查或针对破坏性或高成本操作的用户批准的替代品。

评论

支持 Markdown