When to Use
Use this skill when the task involves creating server-side endpoints in an Expo Router project via +api.ts files. Trigger keywords: "API route", "Expo API", "EAS Hosting", "server function", "backend endpoint", "webhook", "proxy API", "server-side secret".
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
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
Prerequisites
- An Expo Router project with an
app/directory - Expo SDK with API route support
- For deployment:
eas-cliinstalled and an Expo/EAS account
Install EAS CLI globally (Windows PowerShell):
npm install -g eas-cli
eas login
Procedure
1. Create the API route file
API routes live in the app directory with the +api.ts suffix. The file path maps to the URL path.
app/
api/
hello+api.ts -> GET /api/hello
users+api.ts -> /api/users
users/[id]+api.ts -> /api/users/:id
(tabs)/
index.tsx
Create a basic route:
// app/api/hello+api.ts
export function GET(request: Request) {
return Response.json({ message: "Hello from Expo!" });
}
2. Define HTTP methods
Export named functions for each HTTP method you want to support:
// 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 });
}
3. Handle dynamic routes
// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
return Response.json({ userId: id });
}
4. Read query parameters
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 });
}
5. Read headers and authorize
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 });
}
6. Parse JSON body
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 });
}
7. Use environment variables for secrets
Use process.env for server-side secrets. NEVER expose API keys or secrets in client code.
// 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 a
.envfile (never commit it) - EAS Hosting: Use
eas env:createor the Expo dashboard
eas env:create --name OPENAI_API_KEY --value YOUR_KEY --environment production
8. Add CORS headers for web clients
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 });
}
9. Handle errors gracefully
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 });
}
}
10. Test locally
Start the development server with API route support:
npx expo serve
This starts a local server at http://localhost:8081 with full API route support.
Test with curl (PowerShell):
curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{\"name\":\"Test\"}'
11. Deploy to EAS Hosting
eas deploy
This builds and deploys your API routes to EAS Hosting (Cloudflare Workers).
Configure a custom domain in eas.json or the Expo dashboard.
12. Call API routes from the client
// 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" }),
});
EAS Hosting Runtime (Cloudflare Workers)
API routes run on Cloudflare Workers. Respect these limitations:
Missing/Limited APIs
- No Node.js filesystem —
fsmodule 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 of Node APIs
// 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 the 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:
// 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);
}
Common Patterns
Authentication middleware
// 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
// 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());
}
Pitfalls
- NEVER expose API keys or secrets in client code. Keep them in
process.envand reference only inside+api.tsfiles. - No Node.js
fsor native modules on EAS Hosting. The runtime is Cloudflare Workers; use Web APIs (fetch,crypto.subtle,Response,Request). - 30 second CPU timeout. Long-running computations will be killed; offload or chunk them.
- No persistent connections / WebSockets without Durable Objects.
- Always validate and sanitize user input. Never trust request bodies or query params.
- Use correct HTTP status codes: 200, 201, 400, 401, 404, 500.
- Wrap handlers in try/catch and log errors server-side; never leak stack traces to clients.
- One responsibility per endpoint. Keep routes focused.
.envmust never be committed. Add it to.gitignore.- CORS is required for web clients. Browser requests will fail without
Access-Control-Allow-Originand anOPTIONShandler. - File path maps to URL.
app/api/users/[id]+api.tsbecomes/api/users/:id; misnamed files silently 404. - Use TypeScript for type safety to catch request/response shape errors at build time.
Verification
- Confirm the dev server is running:
npx expo serve
Expected: server starts at http://localhost:8081.
- Verify a GET route responds:
curl http://localhost:8081/api/hello
Expected: {"message":"Hello from Expo!"}
- Verify a POST route accepts JSON:
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{\"name\":\"Test\"}'
Expected: JSON response with status 201.
- Verify environment variables are loaded (create a temporary debug route):
// app/api/debug-env+api.ts
export function GET() {
return Response.json({ hasKey: !!process.env.OPENAI_API_KEY });
}
Expected: {"hasKey":true} when .env is present. Remove this route before deploying.
- Verify deployment:
eas deploy
Expected: build completes and a hosting URL is printed. Fetch <deployed-url>/api/hello to confirm.
- Verify production secrets exist:
eas env:list --environment production
Expected: OPENAI_API_KEY (and others) listed.
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
Limitations
- Use this skill only when the task clearly matches its upstream product or API scope.
- Verify commands, API behavior, pricing, quotas, credentials, and deployment effects against current official documentation before making changes.
- Do not treat generated examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.