expo-api-routes

Guidelines for creating API routes in Expo Router with EAS Hosting

31,392 stars
Complexity: easy

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

$curl -o ~/.claude/skills/expo-api-routes/SKILL.md --create-dirs "https://raw.githubusercontent.com/sickn33/antigravity-awesome-skills/main/plugins/antigravity-awesome-skills-claude/skills/expo-api-routes/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/expo-api-routes/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How expo-api-routes Compares

Feature / Agentexpo-api-routesStandard Approach
Platform SupportClaudeLimited / Varies
Context Awareness High Baseline
Installation ComplexityeasyN/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

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 debugging

Related Skills

expo-tailwind-setup

31392
from sickn33/antigravity-awesome-skills

Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling

Mobile DevelopmentClaude

expo-deployment

31392
from sickn33/antigravity-awesome-skills

Deploy Expo apps to production

Mobile DevelopmentClaude

ios-developer

31392
from sickn33/antigravity-awesome-skills

Develop native iOS applications with Swift/SwiftUI. Masters iOS 18, SwiftUI, UIKit integration, Core Data, networking, and App Store optimization.

Mobile DevelopmentClaude

ios-debugger-agent

31392
from sickn33/antigravity-awesome-skills

Debug the current iOS project on a booted simulator with XcodeBuildMCP.

Mobile DevelopmentClaude

earllm-build

31392
from sickn33/antigravity-awesome-skills

Build, maintain, and extend the EarLLM One Android project — a Kotlin/Compose app that connects Bluetooth earbuds to an LLM via voice pipeline.

Mobile DevelopmentClaudeCursorGemini

liquid-glass-design

144923
from affaan-m/everything-claude-code

iOS 26 液态玻璃设计系统 — 适用于 SwiftUI、UIKit 和 WidgetKit 的动态玻璃材质,具有模糊、反射和交互式变形效果。

Mobile DevelopmentClaude

expo-dev-client

31392
from sickn33/antigravity-awesome-skills

Build and distribute Expo development clients locally or via TestFlight

Mobile Development & Build AutomationClaude

expo-cicd-workflows

31392
from sickn33/antigravity-awesome-skills

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.

DevOps & InfrastructureClaude

azure-monitor-opentelemetry-exporter-py

31392
from sickn33/antigravity-awesome-skills

Azure Monitor OpenTelemetry Exporter for Python. Use for low-level OpenTelemetry export to Application Insights.

Observability & MonitoringClaude

nft-standards

31392
from sickn33/antigravity-awesome-skills

Master ERC-721 and ERC-1155 NFT standards, metadata best practices, and advanced NFT features.

Web3 & BlockchainClaude

nextjs-app-router-patterns

31392
from sickn33/antigravity-awesome-skills

Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.

Web FrameworksClaude

new-rails-project

31392
from sickn33/antigravity-awesome-skills

Create a new Rails project

Code GenerationClaude