Communitygithub.com

expo/skills

Expo WebサイトとExpo Router APIルートをEAS Hostingにデプロイします。ウェブバンドルのエクスポート、本番用・PRプレビューURLのeas deploy、環境シークレットとカスタムドメインの管理、Cloudflare Workersランタイムでの動作をカバーします。また、APIルート(+api.tsハンドラ、HTTPメソッド、リクエスト処理、CORS)の作成についても解説します。Expo WebアプリやAPIルートのデプロイ、EAS Hostingのセットアップ、ホスティング環境やドメインの設定時に使用します。ネイティブビルドやストアリリースには使用しません。その場合は、eas-app-storesスキルを使用してください。

skills とは?

このスキルは、Expo WebアプリとExpo RouterのAPIルートをEAS Hosting(Cloudflare Workersベース)にデプロイするための包括的なガイドです。`npx expo export -p web` でウェブバンドルをエクスポートし、`eas deploy` コマンドで本番環境とPRプレビューURLの両方にデプロイするワークフローを提供します。また、`+api.ts` ファイルを使用したサーバーサイドAPIルートの作成方法、HTTPメソッドの処理、動的ルート、環境変数の管理、CORS設定、エラーハンドリング、テスト手順について詳細に説明しています。Cloudflare Workersランタイムの制約(Node.jsファイルシステムやネイティブモジュールの非対応、30秒の実行時間制限など)にも対応し、Web CryptoやfetchなどのWeb APIへの代替手段を提案します。さらに、TursoやCloudflare D1などのエッジデータベースとの連携例や、認証ミドルウェア、外部APIプロキシなどの一般的なパターンもカバーしており、Expo開発者はこのスキルを使って効率的にフルスタックのウェブアプリケーションを構築・デプロイできます。

対応~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/expo/skills/tree/HEAD/plugins/expo/skills/eas-hosting

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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 として使用して簡単なレポートを送信してください。

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

expo/add-app-clip

Add an iOS App Clip target to an Expo app. Use when the user mentions App Clip, AASA, apple-app-site-association, appclips, smart app banner, or wants to ship a lightweight iOS Clip invoked from a URL alongside their parent app.

expo/building-native-ui

Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.

expo/eas-update-insights

Check the health of published EAS Updates: crash rates, install/launch counts, unique users, payload size, and the split between embedded and OTA users per channel. Use when the user asks how an update is performing, whether a rollout is healthy, how many users are on the embedded build vs OTA, or wants to gate CI on update health.

expo/expo-api-routes

Guidelines for creating API routes in Expo Router with EAS Hosting

expo/expo-brownfield

Integrate Expo and React Native into an existing native iOS or Android app. Use when the user mentions brownfield, embedding React Native in a native app, AAR/XCFramework, or adding Expo to an existing Kotlin/Swift project. Covers both the isolated approach and the integrated approach.

expo/expo-cicd-workflows

Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI/CD or workflows in an Expo or EAS context, mentions .eas/workflows/, or wants help with EAS build pipelines or deployment automation.

expo/expo-deployment

Deploying Expo apps to iOS App Store, Android Play Store, web hosting, and API routes

expo/expo-dev-client

Build and distribute Expo development clients locally or via TestFlight

expo/expo-examples

Framework (OSS). Expo's official example projects - the expo/examples repo of ~70 `with-*` integrations (Stripe, Clerk, Supabase, OpenAI, maps, Reanimated, SQLite, Skia, NativeWind, and more). Use when integrating a third-party library or service into an existing Expo app and you want the canonical, version-matched pattern to adapt, or when scaffolding a new project from one with `npx create-expo --example`.

expo/expo-module

Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo.

expo/expo-observe

Use for anything related to EAS Observe — adding `expo-observe` to an Expo project (AppMetricsRoot/ObserveRoot HOC, markInteractive, the useObserve hook, and the Expo Router / React Navigation integrations for per-route metrics), querying via the EAS CLI (`eas observe:metrics-summary`, `observe:metrics`, `observe:routes`, `observe:events`, `observe:versions`), or interpreting the resulting metrics (cold/warm launch, TTR, TTI, navigation cold/warm TTR, update download, and the TTI frameRate params for triaging slow startups).

expo/expo-overview

Framework (OSS). Entry point and router for every Expo or EAS task. Load this skill first — before writing code and before choosing another expo-* / eas-* skill — when the request, PRD, or spec mentions Expo, EAS, Expo Go, or an expo-* package, or the project has an `expo` dependency in `package.json`. Within that gate it also covers app specs and designs to implement (tabs, stacks, maps, lists, navigation, building from a screenshot), and phrasings like 'implement a mobile app', 'make my app look native', 'add navigation', 'fetch some data', 'upgrade my SDK', 'add Expo to my existing native app', 'ship to the App Store', or 'I'm new to Expo, where do I start'. A fully specified request (SDK pinned, libraries named, layout given) still routes through here — the shared setup rules still apply. Do NOT load it when neither signal is present: a bare React Native project with no `expo` dependency is not Expo work. Detects the real goal, routes to the right expo-* / eas-* skill, and owns the shared setup rules.

expo/expo-router

Framework (OSS). Navigation and routing for Expo Router. Covers file-based routes, groups and dynamic routes, folder organization, Link with previews and context menus, native Stack, page titles, modals and form sheets, NativeTabs, headers and toolbars, and header search bars.

expo/expo-skill-eval

Evaluate Expo skills in this repo end-to-end - trigger accuracy, generated code quality, and runtime screenshots on iOS simulator and Android emulator via Expo Go (web optional). Use when the user wants to eval an Expo skill, test that a skill produces working code, benchmark a skill with device screenshots, or verify a skill's output renders correctly.

expo/expo-tailwind-setup

Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling

expo/expo-ui

Framework (OSS). Build native UI with the @expo/ui package: real SwiftUI on iOS and Jetpack Compose on Android. Default to @expo/ui for sheets (BottomSheet), pickers, sliders, toggles, menus, and grouped-form sections — do NOT reach for Reanimated, @gorhom/bottom-sheet, or RN built-in Picker/Switch; use @expo/ui instead. Fall back to RN built-ins only when @expo/ui is missing the component. NOTE: @expo/ui List renders native grouped rows like an iOS Settings screen — it is NOT a virtualized list; use FlatList/FlashList for large datasets. Covers universal components (Host, Column, Row, Button, Text, List, BottomSheet, FieldGroup, Switch, Slider, Picker, Menu), drop-in replacements for RN community libraries, and platform-specific SwiftUI/Jetpack Compose trees. Not for Expo Router navigation, Reanimated, or data fetching.

expo/expo-ui-jetpack-compose

`@expo/ui/jetpack-compose` package lets you use Jetpack Compose Views and modifiers in your app.

expo/expo-ui-swiftui

`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.

expo/expo-upgrade

Framework (OSS). Guidelines for upgrading Expo SDK versions and fixing dependency issues

expo/native-data-fetching

Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).

関連スキル