expo-api-routes
Install this skill
npx skills add expo/skillsWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
Expo API routes allow developers to build server-side logic directly within an Expo project directory using a file-system based routing structure. By defining files with the +api.ts suffix, developers create backend endpoints that execute in a controlled environment, separate from the mobile client. This architecture facilitates secure interaction with sensitive APIs, database operations, and environment-protected services without exposing secrets to the application bundle. It follows standard Request and Response patterns, making it compatible with familiar web-standard APIs. This integrated approach simplifies cross-platform development by unifying client-side UI and server-side logic in a single codebase, specifically targeted for mobile environments running on Expo Application Services.
When to Use This Skill
- β’Masking API keys for third-party services like OpenAI or Stripe
- β’Executing database queries that require administrative permissions
- β’Offloading resource-intensive data processing from the mobile device
- β’Receiving and verifying webhooks from external payment providers
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- βCreate an API route for my Expo app
- βHow to handle POST requests in Expo router
- βHide OpenAI API key in Expo
- βBuild a backend endpoint in my Expo project
- βSet up server-side logic using +api.ts
Pro Tips
- π‘Always use environment variables for secrets (`process.env.EXPO_PUBLIC_MY_VAR`) and configure them correctly for your EAS build to prevent sensitive data from being bundled client-side.
- π‘Structure your API routes logically within the `app/+api` directory, organizing by resource or feature to maintain a scalable and maintainable codebase.
- π‘Implement comprehensive error handling and logging within your API routes to monitor performance, debug issues, and respond gracefully to unexpected situations in production.
What this skill does
- β’Defining server-side endpoints using file-system routing
- β’Processing HTTP methods including GET, POST, PUT, and DELETE
- β’Managing secure server-side secrets via environment variables
- β’Handling request parsing for JSON bodies, query parameters, and headers
- β’Implementing custom CORS headers for cross-origin web requests
When not to use it
- βHandling high-frequency real-time updates or WebSocket communication
- βExposing public data that could be fetched directly from a client
- βManaging complex user authentication flows better suited for dedicated Auth providers
Example workflow
- Create an app/api/data+api.ts file
- Export a POST function to handle incoming requests
- Access process.env variables inside the handler
- Return data using the Response.json helper
- Deploy the project to EAS Hosting to enable the backend
- Call the endpoint from the mobile client using fetch
Prerequisites
- βExpo Router installed
- βEAS CLI configured for deployment
- βNode.js environment
Pitfalls & limitations
- !API routes only function when deployed to a server environment via EAS
- !Environment variables are server-side only and remain inaccessible to mobile client code
- !Direct file uploads are inefficient; use presigned URLs for storage instead
FAQ
How it compares
Unlike manual backend setups like Express or separate microservices, Expo API routes keep your backend logic co-located with your frontend, reducing context switching and simplifying project structure.
π Full skill instructions β original source: expo/skills
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
// 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:
// 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
// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
return Response.json({ userId: id });
}## Request Handling
### 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 });
}### Headers
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
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:// 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:
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
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:
npx expo serveThis starts a local server at
http://localhost:8081 with full API route support.Test with curl:
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
npm install -g eas-cli
eas login### Deploy
eas deployThis builds and deploys your API routes to EAS Hosting (Cloudflare Workers).
### Environment Variables for Production
# 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
// 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:
// 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
// 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
// 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());
}## 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
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/expo-api-routes/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/expo/skills/expo-api-routes/SKILL.md - Cursor:
~/.cursor/skills/expo/skills/expo-api-routes/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/expo/skills/expo-api-routes/SKILL.md
π Install with CLI:npx skills add expo/skills
