EAS Hosting
EASサービス - 有料です。 EAS Hostingは有料のExpo Application Services製品で、無料枠の制限があります。本番デプロイは、プランのリクエスト数と帯域幅の割り当てを消費します。詳細は https://expo.dev/pricing をご覧ください。APIルートの作成とウェブバンドルのエクスポートは無料でオープンソースであり、エクスポートされたサーバー出力をEAS Hostingではなくセルフホスティングすることもできます。
EAS Hostingは、ExpoのWebアプリとAPIルートをExpoのマネージドエッジ(Cloudflare Workers)にデプロイします。npx expo export -p web でウェブバンドルをエクスポートし、eas deploy でデプロイします。同じコマンドで、一緒にバンドルされたExpo Router APIルートもデプロイされます。このスキルは、Webサイトのデプロイ、APIルートの作成、ホスティングランタイムをカバーしています。デプロイのワークフローについては、以下の「デプロイ」セクションを参照してください。
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メソッドに対応する名前付き関数をエクスポートします:
// 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ルートを含む開発サーバーを起動:
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
デプロイ
デプロイはウェブバンドルとExpo Router APIルートを一緒に送信します。eas deploy が両方を処理します。エクスポートは、フルWebサイト、APIルートのみのバックエンド、またはその両方のいずれの場合でも実行されます。
# ウェブバンドルをエクスポート(APIルートを含む)
npx expo export -p web
# プレビューデプロイ(PR形式のURL)
npx eas-cli@latest deploy
# 本番環境にデプロイ
npx eas-cli@latest deploy --prod
すべてがEAS Hosting(Cloudflare Workers)に配置されます。
本番環境用の環境変数
# シークレットを作成
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production
# またはExpoダッシュボードを使用
カスタムドメイン
eas.json またはExpoダッシュボードで設定します。
EAS Workflowsでの自動化
type: deploy ワークフローを使用して、mainへのプッシュごとにWebサイト(およびAPIルート)をデプロイします:
.eas/workflows/deploy.yml
name: Deploy
on:
push:
branches:
- main
# https://docs.expo.dev/eas/workflows/syntax/#deploy
jobs:
deploy_web:
type: deploy
params:
prod: true
プルリクエストのプレビューデプロイは、同じジョブタイプで prod: false を使用します:
name: Web PR Preview
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
type: deploy
params:
prod: false
これらの例以外のワークフローYAMLを作成または検証するには、eas-workflows スキルを使用してください。
EAS Hostingランタイム(Cloudflare Workers)
APIルートはCloudflare Workers上で実行されます。主な制限事項:
欠落/制限のあるAPI
- Node.jsファイルシステムなし —
fsモジュールは利用不可 - ネイティブNodeモジュールなし — Web APIまたはポリフィルを使用
- 実行時間の制限 — CPU集中タスクは30秒のタイムアウト
- 永続的な接続なし — WebSocketにはDurable Objectsが必要
- fetchは利用可能 — HTTPリクエストには標準のfetchを使用
代わりにWeb APIを使用
// Node 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ルート呼び出し
// 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ルートは集中させる — エンドポイントごとに1つの責務
- 型安全性のためにTypeScriptを使用する
- デバッグのためにサーバーサイドでエラーをログに記録する
フィードバックの送信
このスキルにエラー、誤解を招く情報、または古い情報を見つけた場合は、Expoが改善できるように報告してください:
npx --yes @expo/agent-cli@latest feedback --category skills --subject "eas-hosting" "<アクション可能なフィードバック>"
報告する具体的で実行可能な内容がある場合にのみ送信してください。関連するコンテキストをできるだけ多く含めてください。
エージェントがExpoタスクで繰り返し失敗する場合、またはユーザーが介入する必要がある場合は、タスク、期待される結果、および問題の内容を --category evals と関連するExpoパッケージまたはコマンドを --subject として使用して簡単なレポートを送信してください。