--- name: expo-api-routes description: Guidelines for creating API routes in Expo Router with EAS Hosting version: 1.0.0 license: MIT --- ## 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 ```ts // 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: ```ts // 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 ```ts // app/api/users/[id]+api.ts export function GET(request: Request, { id }: { id: string }) { return Response.json({ userId: id }); } ``` ## Request Handling ### Query Parameters ```ts 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 ```ts 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 ```ts 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: ```ts // 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: ```ts 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 ```ts 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: ```bash npx expo serve ``` This starts a local server at `http://localhost:8081` with full API route support. Test with curl: ```bash 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 ```bash npm install -g eas-cli eas login ``` ### Deploy ```bash eas deploy ``` This builds and deploys your API routes to EAS Hosting (Cloudflare Workers). ### Environment Variables for Production ```bash # 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 filesystem** — `fs` 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 ```ts // 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: ```ts // 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 ```ts // 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 ```ts // 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 ```ts // 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