claude-skills/

Anthropic公式スキル・プラグインの日本語ディレクトリ

last sync 22h ago
スキルOfficialdevelopment

🛣️expo-api-routes

プラグイン
expo
ライセンス
MIT

説明

EAS HostingでExpo RouterのAPIルートを作成するためのガイドライン

原文を表示

Guidelines for creating API routes in Expo Router with EAS Hosting

ユースケース

  • Expo RouterのAPIルート構築時
  • EAS Hostingでの実装方法を確認するとき

本文(日本語訳)

API Routeを使用すべき場面

次のような場合に使用:

  • サーバーサイドのシークレット — クライアントに絶対に渡してはいけないAPIキー、データベース認証情報、トークン
  • データベース操作 — 外部に公開すべきでない直接のデータベースクエリ
  • サードパーティAPIのプロキシ — 外部サービス(OpenAI、Stripeなど)を呼び出す際のAPIキー隠蔽
  • サーバーサイドバリデーション — データベース書き込み前のデータ検証
  • Webhookエンドポイント — StripeやGitHubなどのサービスからのコールバック受信
  • レートリミット — サーバーレベルでのアクセス制御
  • 重い計算処理 — モバイル端末では遅くなる処理のオフロード

API Routeを使用すべきでない場面

次のような場合は避けてください:

  • データがすでに公開されている — 代わりにパブリックAPIへ直接fetchを使用
  • シークレットが不要 — 静的データやクライアントで安全に扱える操作
  • リアルタイム更新が必要 — WebSocketまたはSupabase Realtimeなどのサービスを使用
  • シンプルなCRUD — マネージドバックエンドとしてFirebase、Supabase、Convexを検討
  • ファイルアップロード — ストレージへの直接アップロード(S3のpresigned URL、Cloudflare R2)を使用
  • 認証のみ — Clerk、Auth0、またはFirebase Authを使用

ファイル構成

API Routeは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 Route

// 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();
    // 処理...
    return Response.json({ success: true });
  } catch (error) {
    console.error("API error:", error);
    return Response.json({ error: "Internal server error" }, { status: 500 });
  }
}

ローカルでのテスト

API Routeを含む開発サーバーを起動します:

npx expo serve

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

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 RouteがビルドされEAS Hosting(Cloudflare Workers)にデプロイされます。

本番環境の環境変数

# シークレットを作成
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production

# またはExpoダッシュボードを使用

カスタムドメイン

eas.jsonまたはExpoダッシュボードで設定します。

EAS Hostingのランタイム(Cloudflare Workers)

API RouteはCloudflare Workers上で動作します。主な制約事項:

利用できない/制限されているAPI

  • Node.jsのファイルシステムなしfsモジュールは使用不可
  • ネイティブのNode.jsモジュールなし — Web APIまたはポリフィルを使用
  • 実行時間の制限 — CPU集約的なタスクは30秒でタイムアウト
  • 永続的な接続なし — WebSocketにはDurable Objectsが必要
  • fetchは利用可能 — HTTPリクエストには標準のfetchを使用

代わりにWeb APIを使用する

// Node.js の crypto の代わりに Web Crypto を使用
const hash = await crypto.subtle.digest(
  "SHA-256",
  new TextEncoder().encode("data")
);

// node-fetch の代わりに fetch を使用
const response = await fetch("https://api.example.com");

// Response/Request はそのまま利用可能
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 Routeを呼び出す

// React Nativeコンポーネントから
const response = await fetch("/api/hello");
const data = await response.json();

// ボディ付きで送信
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" },
    });
  }

  // トークンを検証...
  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 Routeの責務を単一に保つ — 1エンドポイントにつき1つの役割
  • 型安全性のためにTypeScriptを使用する
  • デバッグのためにエラーはサーバーサイドでログに記録する
原文(English)を表示

When to Use API Routes

Use API routes when you need:

  • Server-side secrets — API keys, database credentials, or tokens that must never reach the client
  • Database operations — Direct database queries that shouldn't be exposed
  • Third-party API proxies — Hide API keys when calling external services (OpenAI, Stripe, etc.)
  • Server-side validation — Validate data before database writes
  • Webhook endpoints — Receive callbacks from services like Stripe or GitHub
  • Rate limiting — Control access at the server level
  • Heavy computation — Offload processing that would be slow on mobile

When NOT to Use API Routes

Avoid API routes when:

  • Data is already public — Use direct fetch to public APIs instead
  • No secrets required — Static data or client-safe operations
  • Real-time updates needed — Use WebSockets or services like Supabase Realtime
  • Simple CRUD — Consider Firebase, Supabase, or Convex for managed backends
  • File uploads — Use direct-to-storage uploads (S3 presigned URLs, Cloudflare R2)
  • Authentication only — Use Clerk, Auth0, or Firebase Auth instead

File Structure

API routes live in the app directory with +api.ts suffix:

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

Basic API Route

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

HTTP Methods

Export named functions for each HTTP method:

// 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 });
}

Dynamic Routes

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

Request Handling

Query Parameters

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 });
}

Headers

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 Body

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 });
}

Environment Variables

Use process.env for server-side secrets:

// 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);
}

Set environment variables:

  • Local: Create .env file (never commit)
  • EAS Hosting: Use eas env:create or Expo dashboard

CORS Headers

Add CORS for web clients:

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 });
}

Error Handling

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 });
  }
}

Testing Locally

Start the development server with API routes:

npx expo serve

This starts a local server at http://localhost:8081 with full API route support.

Test with curl:

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

Deployment to EAS Hosting

Prerequisites

npm install -g eas-cli
eas login

Deploy

eas deploy

This builds and deploys your API routes to EAS Hosting (Cloudflare Workers).

Environment Variables for Production

# Create a secret
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production

# Or use the Expo dashboard

Custom Domain

Configure in eas.json or Expo dashboard.

EAS Hosting Runtime (Cloudflare Workers)

API routes run on Cloudflare Workers. Key limitations:

Missing/Limited APIs

  • No Node.js filesystemfs module unavailable
  • No native Node modules — Use Web APIs or polyfills
  • Limited execution time — 30 second timeout for CPU-intensive tasks
  • No persistent connections — WebSockets require Durable Objects
  • fetch is available — Use standard fetch for HTTP requests

Use Web APIs Instead

// 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" },
});

Database Options

Since filesystem is unavailable, use cloud databases:

  • Cloudflare D1 — SQLite at the edge
  • Turso — Distributed SQLite
  • PlanetScale — Serverless MySQL
  • Supabase — Postgres with REST API
  • Neon — Serverless Postgres

Example with 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);
}

Calling API Routes from Client

// 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" }),
});

Common Patterns

Authentication Middleware

// 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 });
}

Proxy External 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());
}

Rules

  • NEVER expose API keys or secrets in client code
  • ALWAYS validate and sanitize user input
  • Use proper HTTP status codes (200, 201, 400, 401, 404, 500)
  • Handle errors gracefully with try/catch
  • Keep API routes focused — one responsibility per endpoint
  • Use TypeScript for type safety
  • Log errors server-side for debugging

原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。