Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build Expo API routes and server-side endpoints using Expo Router's file-based server functions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: expo-api-routes3description: Guidelines for creating API routes in Expo Router with EAS Hosting4version: 1.0.05license: MIT6---78## When to Use API Routes910Use API routes when you need:1112- **Server-side secrets** — API keys, database credentials, or tokens that must never reach the client13- **Database operations** — Direct database queries that shouldn't be exposed14- **Third-party API proxies** — Hide API keys when calling external services (OpenAI, Stripe, etc.)15- **Server-side validation** — Validate data before database writes16- **Webhook endpoints** — Receive callbacks from services like Stripe or GitHub17- **Rate limiting** — Control access at the server level18- **Heavy computation** — Offload processing that would be slow on mobile1920## When NOT to Use API Routes2122Avoid API routes when:2324- **Data is already public** — Use direct fetch to public APIs instead25- **No secrets required** — Static data or client-safe operations26- **Real-time updates needed** — Use WebSockets or services like Supabase Realtime27- **Simple CRUD** — Consider Firebase, Supabase, or Convex for managed backends28- **File uploads** — Use direct-to-storage uploads (S3 presigned URLs, Cloudflare R2)29- **Authentication only** — Use Clerk, Auth0, or Firebase Auth instead3031## File Structure3233API routes live in the `app` directory with `+api.ts` suffix:3435```36app/37api/38hello+api.ts → GET /api/hello39users+api.ts → /api/users40users/[id]+api.ts → /api/users/:id41(tabs)/42index.tsx43```4445## Basic API Route4647```ts48// app/api/hello+api.ts49export function GET(request: Request) {50return Response.json({ message: "Hello from Expo!" });51}52```5354## HTTP Methods5556Export named functions for each HTTP method:5758```ts59// app/api/items+api.ts60export function GET(request: Request) {61return Response.json({ items: [] });62}6364export async function POST(request: Request) {65const body = await request.json();66return Response.json({ created: body }, { status: 201 });67}6869export async function PUT(request: Request) {70const body = await request.json();71return Response.json({ updated: body });72}7374export async function DELETE(request: Request) {75return new Response(null, { status: 204 });76}77```7879## Dynamic Routes8081```ts82// app/api/users/[id]+api.ts83export function GET(request: Request, { id }: { id: string }) {84return Response.json({ userId: id });85}86```8788## Request Handling8990### Query Parameters9192```ts93export function GET(request: Request) {94const url = new URL(request.url);95const page = url.searchParams.get("page") ?? "1";96const limit = url.searchParams.get("limit") ?? "10";9798return Response.json({ page, limit });99}100```101102### Headers103104```ts105export function GET(request: Request) {106const auth = request.headers.get("Authorization");107108if (!auth) {109return Response.json({ error: "Unauthorized" }, { status: 401 });110}111112return Response.json({ authenticated: true });113}114```115116### JSON Body117118```ts119export async function POST(request: Request) {120const { email, password } = await request.json();121122if (!email || !password) {123return Response.json({ error: "Missing fields" }, { status: 400 });124}125126return Response.json({ success: true });127}128```129130## Environment Variables131132Use `process.env` for server-side secrets:133134```ts135// app/api/ai+api.ts136export async function POST(request: Request) {137const { prompt } = await request.json();138139const response = await fetch("https://api.openai.com/v1/chat/completions", {140method: "POST",141headers: {142"Content-Type": "application/json",143Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,144},145body: JSON.stringify({146model: "gpt-4",147messages: [{ role: "user", content: prompt }],148}),149});150151const data = await response.json();152return Response.json(data);153}154```155156Set environment variables:157158- **Local**: Create `.env` file (never commit)159- **EAS Hosting**: Use `eas env:create` or Expo dashboard160161## CORS Headers162163Add CORS for web clients:164165```ts166const corsHeaders = {167"Access-Control-Allow-Origin": "*",168"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",169"Access-Control-Allow-Headers": "Content-Type, Authorization",170};171172export function OPTIONS() {173return new Response(null, { headers: corsHeaders });174}175176export function GET() {177return Response.json({ data: "value" }, { headers: corsHeaders });178}179```180181## Error Handling182183```ts184export async function POST(request: Request) {185try {186const body = await request.json();187// Process...188return Response.json({ success: true });189} catch (error) {190console.error("API error:", error);191return Response.json({ error: "Internal server error" }, { status: 500 });192}193}194```195196## Testing Locally197198Start the development server with API routes:199200```bash201npx expo serve202```203204This starts a local server at `http://localhost:8081` with full API route support.205206Test with curl:207208```bash209curl http://localhost:8081/api/hello210curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'211```212213## Deployment to EAS Hosting214215### Prerequisites216217```bash218npm install -g eas-cli219eas login220```221222### Deploy223224```bash225eas deploy226```227228This builds and deploys your API routes to EAS Hosting (Cloudflare Workers).229230### Environment Variables for Production231232```bash233# Create a secret234eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production235236# Or use the Expo dashboard237```238239### Custom Domain240241Configure in `eas.json` or Expo dashboard.242243## EAS Hosting Runtime (Cloudflare Workers)244245API routes run on Cloudflare Workers. Key limitations:246247### Missing/Limited APIs248249- **No Node.js filesystem** — `fs` module unavailable250- **No native Node modules** — Use Web APIs or polyfills251- **Limited execution time** — 30 second timeout for CPU-intensive tasks252- **No persistent connections** — WebSockets require Durable Objects253- **fetch is available** — Use standard fetch for HTTP requests254255### Use Web APIs Instead256257```ts258// Use Web Crypto instead of Node crypto259const hash = await crypto.subtle.digest(260"SHA-256",261new TextEncoder().encode("data")262);263264// Use fetch instead of node-fetch265const response = await fetch("https://api.example.com");266267// Use Response/Request (already available)268return new Response(JSON.stringify(data), {269headers: { "Content-Type": "application/json" },270});271```272273### Database Options274275Since filesystem is unavailable, use cloud databases:276277- **Cloudflare D1** — SQLite at the edge278- **Turso** — Distributed SQLite279- **PlanetScale** — Serverless MySQL280- **Supabase** — Postgres with REST API281- **Neon** — Serverless Postgres282283Example with Turso:284285```ts286// app/api/users+api.ts287import { createClient } from "@libsql/client/web";288289const db = createClient({290url: process.env.TURSO_URL!,291authToken: process.env.TURSO_AUTH_TOKEN!,292});293294export async function GET() {295const result = await db.execute("SELECT * FROM users");296return Response.json(result.rows);297}298```299300## Calling API Routes from Client301302```ts303// From React Native components304const response = await fetch("/api/hello");305const data = await response.json();306307// With body308const response = await fetch("/api/users", {309method: "POST",310headers: { "Content-Type": "application/json" },311body: JSON.stringify({ name: "John" }),312});313```314315## Common Patterns316317### Authentication Middleware318319```ts320// utils/auth.ts321export async function requireAuth(request: Request) {322const token = request.headers.get("Authorization")?.replace("Bearer ", "");323324if (!token) {325throw new Response(JSON.stringify({ error: "Unauthorized" }), {326status: 401,327headers: { "Content-Type": "application/json" },328});329}330331// Verify token...332return { userId: "123" };333}334335// app/api/protected+api.ts336import { requireAuth } from "../../utils/auth";337338export async function GET(request: Request) {339const { userId } = await requireAuth(request);340return Response.json({ userId });341}342```343344### Proxy External API345346```ts347// app/api/weather+api.ts348export async function GET(request: Request) {349const url = new URL(request.url);350const city = url.searchParams.get("city");351352const response = await fetch(353`https://api.weather.com/v1/current?city=${city}&key=${process.env.WEATHER_API_KEY}`354);355356return Response.json(await response.json());357}358```359360## Rules361362- NEVER expose API keys or secrets in client code363- ALWAYS validate and sanitize user input364- Use proper HTTP status codes (200, 201, 400, 401, 404, 500)365- Handle errors gracefully with try/catch366- Keep API routes focused — one responsibility per endpoint367- Use TypeScript for type safety368- Log errors server-side for debugging369