expo-api-routes
Guidelines for creating API routes in Expo Router with EAS Hosting
About this skill
This skill equips an AI agent with detailed knowledge regarding the strategic implementation of API routes within Expo Router projects, particularly when hosted via EAS Hosting. It outlines critical use cases where server-side execution is paramount, such as safeguarding sensitive information like API keys and database credentials, executing direct database operations, securely proxying calls to third-party APIs (e.g., OpenAI, Stripe), performing robust server-side data validation, establishing webhook endpoints for external services, implementing rate limiting, and offloading computationally intensive tasks from mobile clients. By understanding these guidelines, the AI agent can effectively advise developers, assist in architectural design, generate relevant code patterns, or troubleshoot issues related to secure and scalable backend integrations for mobile applications.
Best use case
Assisting developers in making informed architectural decisions for their Expo applications by identifying scenarios that necessitate secure server-side API routes. This includes generating boilerplate code for specific backend functionalities (e.g., Stripe webhooks, database interactions), advising on best practices for secret management, and explaining the rationale behind using API proxies for external services.
Guidelines for creating API routes in Expo Router with EAS Hosting
An AI agent will provide accurate, secure, and performant guidance or code suggestions for implementing server-side logic in Expo applications using API routes. This could manifest as architectural recommendations, code snippets for specific API endpoints (e.g., a secure payment webhook), or explanations of best practices for protecting sensitive data and managing external API integrations.
Practical example
Example input
I need to add a secure API endpoint to my Expo app that receives webhooks from an external service and processes them without exposing any secrets. How should I approach this using Expo Router API routes?
Example output
```json
{
"guidance": "To create a secure API endpoint for receiving webhooks in your Expo app using Expo Router API routes, consider the following:
1. **Create a dedicated API route**: Define a new file in your `app/api/` directory (e.g., `app/api/my-webhook.js`) that will handle `POST` requests.
2. **Secure Secret Handling**: Store your webhook secret (provided by the external service) as an environment variable (e.g., `WEBHOOK_SECRET`) on your EAS Hosting configuration. Access it within your API route using `process.env.WEBHOOK_SECRET`.
3. **Signature Verification**: Always verify the authenticity of incoming webhooks. Most services send a signature in a request header. Use a relevant library (e.g., `crypto` for HMAC-SHA256) to re-compute the signature with the raw request body and your secret, then compare it to the received signature. Reject requests with invalid signatures.
4. **Raw Body Access**: Ensure you can access the raw request body, as signature verification often requires it before it's parsed as JSON.
5. **Event Processing**: Once verified, parse the webhook payload and implement your application logic based on the event type (e.g., update database, send notifications).
6. **Response**: Return an appropriate HTTP status code (e.g., 200 OK for success, 400 Bad Request for verification failure, 500 Internal Server Error for processing issues) to the external service.
Here's a conceptual structure for `app/api/my-webhook.js`:
```javascript
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
export async function POST(req: ExpoRequest) {
if (!WEBHOOK_SECRET) {
console.error('Webhook secret not configured.');
return ExpoResponse.json({ error: 'Server configuration error' }, { status: 500 });
}
const rawBody = await req.text(); // Get raw body for signature verification
const signature = req.headers.get('x-my-service-signature'); // Example header
if (!signature) {
return ExpoResponse.json({ error: 'Missing signature' }, { status: 400 });
}
try {
// Example: Verify signature (adapt based on your service's specifics)
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
hmac.update(rawBody);
const expectedSignature = `sha256=${hmac.digest('hex')}`;
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
console.warn('Invalid webhook signature');
return ExpoResponse.json({ error: 'Invalid signature' }, { status: 403 });
}
const payload = JSON.parse(rawBody);
// Process the webhook event here
console.log('Received verified webhook event:', payload.type);
// Example: if (payload.type === 'event.completed') { /* ... */ }
return ExpoResponse.json({ received: true }, { status: 200 });
} catch (error) {
console.error('Webhook processing error:', error);
return ExpoResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
```
This setup ensures your webhook endpoint is secure and processes events reliably without exposing sensitive secrets."
}
```When to use this skill
- **Server-side secrets**: When dealing with API keys, database credentials, or authentication tokens that must never be exposed on the client-side.
- **Database operations**: For performing direct, secure queries and manipulations against a database.
- **Third-party API proxies**: To securely call external services (like OpenAI or Stripe) by hiding API keys and managing requests on the server.
- **Server-side validation**: For validating data integrity and security before processing or persisting it to a database.
When not to use this skill
- When the task involves purely client-side operations that do not handle sensitive data, require heavy computation, or interact with external services needing a proxy.
- When the primary goal is to perform simple data fetching that can be securely handled directly from the client with appropriate authentication headers and without exposing sensitive backend logic.
- When an agent is asked to perform a task that is strictly UI-related or involves direct, real-time user interaction better suited for client-side processing without server roundtrips.
- If the solution can be implemented entirely within the Expo client without any security or performance benefits from server-side intervention.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/expo-api-routes/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How expo-api-routes Compares
| Feature / Agent | expo-api-routes | Standard Approach |
|---|---|---|
| Platform Support | Claude | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | easy | N/A |
Frequently Asked Questions
What does this skill do?
Guidelines for creating API routes in Expo Router with EAS Hosting
Which AI agents support this skill?
This skill is designed for Claude.
How difficult is it to install?
The installation complexity is rated as easy. You can find the installation instructions above.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
Related Guides
Best AI Skills for Claude
Explore the best AI skills for Claude and Claude Code across coding, research, workflow automation, documentation, and agent operations.
AI Agents for Coding
Browse AI agent skills for coding, debugging, testing, refactoring, code review, and developer workflows across Claude, Cursor, and Codex.
ChatGPT vs Claude for Agent Skills
Compare ChatGPT and Claude for AI agent skills across coding, writing, research, and reusable workflow execution.
SKILL.md Source
## 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 debuggingRelated Skills
expo-tailwind-setup
Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling
expo-deployment
Deploy Expo apps to production
ios-developer
Develop native iOS applications with Swift/SwiftUI. Masters iOS 18, SwiftUI, UIKit integration, Core Data, networking, and App Store optimization.
ios-debugger-agent
Debug the current iOS project on a booted simulator with XcodeBuildMCP.
earllm-build
Build, maintain, and extend the EarLLM One Android project — a Kotlin/Compose app that connects Bluetooth earbuds to an LLM via voice pipeline.
liquid-glass-design
iOS 26 液态玻璃设计系统 — 适用于 SwiftUI、UIKit 和 WidgetKit 的动态玻璃材质,具有模糊、反射和交互式变形效果。
expo-dev-client
Build and distribute Expo development clients locally or via TestFlight
expo-cicd-workflows
Helps understand and write EAS workflow YAML files for Expo projects. Use this skill when the user asks about CI/CD or workflows in an Expo or EAS context, mentions .eas/workflows/, or wants help with EAS build pipelines or deployment automation.
azure-monitor-opentelemetry-exporter-py
Azure Monitor OpenTelemetry Exporter for Python. Use for low-level OpenTelemetry export to Application Insights.
nft-standards
Master ERC-721 and ERC-1155 NFT standards, metadata best practices, and advanced NFT features.
nextjs-app-router-patterns
Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.
new-rails-project
Create a new Rails project