eas-hosting について
Expo 開発者向けに、EAS Hosting で Web アプリと API ルートを公開する方法を解説するスキルです。ルートの作成、実行環境の制約、デプロイに加え、出力したサーバー成果物のセルフホストも扱います。
主な機能
- 01Expo Router API ルートの作成
- 02シークレットのサーバー側管理
- 03リクエスト、CORS、エラーの処理
- 04ローカルでのルートテスト
- 05Web バンドルとルートのデプロイ
活用シーン
- 01キーを公開せずに外部 API を中継
- 02Webhook の受信とサーバー側検証
- 03API ルート付き Expo サイトの公開
- 04サーバー成果物の出力とセルフホスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ expo/ skills --skill eas-hostingインストール後、Claude Code で/eas-hostingを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
EAS Hosting
EAS サービスは有料です。 EAS Hosting は無料枠の制限がある有料の Expo Application Services 製品です。本番環境へのデプロイでは、ご利用プランのリクエスト数と帯域幅の上限が適用されます。https://expo.dev/pricing をご覧ください。API ルートの作成と Web バンドルのエクスポートは無料で、オープンソースです。また、エクスポートしたサーバー出力を EAS Hosting ではなく、ご自身でホストすることもできます。
EAS Hosting は、Expo の管理するエッジ(Cloudflare Workers)に Expo のWeb アプリと API ルートをデプロイします。npx expo export -p web で Web バンドルをエクスポートし、eas deploy でデプロイします。同じコマンドで、バンドルに含まれる Expo Router の API ルートもデプロイされます。このスキルでは、Web サイトのデプロイ、API ルートの作成、ホスティングのランタイムについて説明します。デプロイのワークフローについては、以下の「デプロイ」セクションをご覧ください。
API ルートを使用する場合
次のような場合は、API ルートを使用してください。
- サーバーサイドのシークレット — クライアントに決して渡してはならない API キー、データベースの認証情報、トークン
- データベース操作 — 公開すべきでないデータベースへの直接クエリ
- サードパーティ API のプロキシ — 外部サービス(OpenAI、Stripe など)を呼び出す際に API キーを隠す
- サーバーサイドの検証 — データベースへの書き込み前にデータを検証する
- Webhook エンドポイント — Stripe や GitHub などのサービスからコールバックを受け取る
- レート制限 — サーバーレベルでアクセスを制御する
- 負荷の高い計算処理 — モバイルでは時間がかかる処理をオフロードする
API ルートを使用しない場合
次のような場合は、API ルートの使用を避けてください。
- データがすでに公開されている — 代わりに公開 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
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
デプロイ
デプロイすると、Web バンドルと Expo Router の API ルートがまとめて公開されます。eas deploy が両方を処理します。Web サイト全体がある場合も、API ルートのみのバックエンドの場合も、その両方がある場合も、エクスポートは実行されます。
# Export the web bundle (includes any API routes)
npx expo export -p web
# Deploy a preview (PR-style URL)
npx eas-cli@latest deploy
# Deploy to production
npx eas-cli@latest deploy --prod
すべて 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 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/制限のある 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 を使用してください
- デバッグ用に、サーバー側でエラーをログに記録してください
フィードバックの送信
このスキルにエラー、誤解を招く情報、または古い情報があった場合は、Expo の改善に役立てるため報告してください。
npx --yes @expo/agent-cli@latest feedback --category skills --subject "eas-hosting" "<actionable feedback>"
具体的で実行可能な報告内容がある場合に限り、送信してください。関連するコンテキストをできるだけ詳しく記載してください。
エージェントが Expo のタスクを繰り返し失敗する場合や、ユーザーが介入しなければならない場合は、タスクの内容、期待される結果、問題の内容を簡潔にまとめ、--category evals と、関連する Expo パッケージまたはコマンドを --subject に指定して報告してください。