Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guidelines for creating API routes in Expo Router with EAS Hosting
.claude/skills/expo-api-routes/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✗→✗ | = Same ✗ | — | — |
| case-14 | ✗→✗ | = Same ✗ | — | — |
| case-04 | ✗→✗ | = Same ✗ | — | — |
Use API routes when you need:
Avoid API routes when:
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.tsxts// app/api/hello+api.ts export function GET(request: Request) { return Response.json({ message: "Hello from Expo!" }); }
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 }); }
ts// app/api/users/[id]+api.ts export function GET(request: Request, { id }: { id: string }) { return Response.json({ userId: id }); }
tsexport 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 }); }
tsexport function GET(request: Request) { const auth = request.headers.get("Authorization"); if (!auth) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } return Response.json({ authenticated: true }); }
tsexport 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 }); }
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:
.env file (never commit)eas env:create or Expo dashboardAdd CORS for web clients:
tsconst 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 }); }
tsexport 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 }); } }
Start the development server with API routes:
bashnpx expo serve
This starts a local server at http://localhost:8081 with full API route support.
Test with curl:
bashcurl http://localhost:8081/api/hello curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'
bashnpm install -g eas-cli eas login
basheas deploy
This builds and deploys your API routes to EAS Hosting (Cloudflare Workers).
bash# Create a secret eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production # Or use the Expo dashboard
Configure in eas.json or Expo dashboard.
API routes run on Cloudflare Workers. Key limitations:
fs module unavailablets// 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" }, });
Since filesystem is unavailable, use cloud databases:
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); }
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" }), });
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 }); }
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()); }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.