mcpField
E
expo-api-routes

expo-api-routes

github.com/sickn33開発49.0k49 回インストール今日更新
リポジトリ

expo-api-routes について

Expo アプリ開発者向けに、API Routes の定義、リクエスト処理、デプロイ方法を案内するガイドです。サーバー側の秘密情報、入力検証、プロキシ利用に加え、EAS Hosting の実行環境の制約も扱います。

主な機能

  1. 01静的・動的 API Routes の定義
  2. 02メソッド、パラメーター、本文の処理
  3. 03環境変数によるサーバー秘密情報の管理
  4. 04CORS とエラー処理の設定
  5. 05ローカルテストと EAS Hosting へのデプロイ
  6. 06Cloudflare Workers の実行制限の確認

活用シーン

  1. 01サードパーティ API キーのサーバー側管理
  2. 02Webhook の受信と送信データの検証
  3. 03モバイルアプリ向けサーバーエンドポイントの提供

インストール

コマンドをコピーしてターミナルで実行します。

$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 ルートを避けます。

  • データがすでに公開されている — 代わりに公開 API への直接 fetch を使用する
  • シークレットが不要 — 静的データまたはクライアントで安全に扱える操作
  • リアルタイム更新が必要 — 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 メソッドごとに関数を named export します。

// 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

これにより、完全な API ルートサポートを備えたローカルサーバーが http://localhost:8081 で起動します。

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 またはポリフィルを使用してください
  • 実行時間の制限 — CPU を集中的に使うタスクは 30 秒でタイムアウトします
  • 永続的な接続なし — WebSocket には Durable Objects が必要です
  • fetch は利用可能 — HTTP リクエストには標準の fetch を使用してください
代わりに 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 ルートは焦点を絞ること — エンドポイントごとに 1 つの責務
  • 型安全性のために TypeScript を使用すること
  • デバッグのためにサーバー側でエラーをログに記録すること

制限事項

  • このスキルは、タスクがその上流製品または API のスコープに明確に一致する場合にのみ使用してください。
  • 変更を行う前に、コマンド、API の動作、料金、クォータ、認証情報、デプロイの影響を、最新の公式ドキュメントに照らして確認してください。
  • 生成された例は、環境固有のテスト、セキュリティレビュー、または破壊的・高コストな操作に対するユーザーの承認の代わりにはならないものとして扱ってください。

コメント