ChatGPT Claude Codex CLI Cohere Cursor DeepSeek Gemini GitHub Copilot GLM Grok Kimi Llama MiniMax Mistral OpenAI opencode Skill

expo-api-routes

Guidelines for creating API routes in Expo Router with EAS Hosting

LLM Mart · 0 points · 20 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download sickn33-agentic-awesome-skills-skills_expo-api-routes-286166a.zip · 3 KB
Part of sickn33/agentic-awesome-skills — 427 skills
This skill couldn't be refreshed from GitHub on the last check — you're seeing the last imported snapshot.

Install

skills CLI npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/expo-api-routes
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart
Git git clone https://github.com/sickn33/agentic-awesome-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole sickn33/agentic-awesome-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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

// 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 serve

This 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 deploy

This 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

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.
Files (agentic-awesome-skills)
  • agents
    • openai.yaml 353 B
      interface:
        display_name: "Expo API Routes"
        short_description: "Create Expo Router API routes for server-side secrets, validation, webhooks, proxies, and EAS Hosting"
        default_prompt: "Use $expo-api-routes to design, implement, test, and deploy Expo Router API routes, especially when secrets or server-side validation must stay out of the client."
      
  • SKILL.md 9.1 KB
    ---
    name: expo-api-routes
    description: Guidelines for creating API routes in Expo Router with EAS Hosting
    risk: critical
    source: https://github.com/expo/skills/tree/main/plugins/expo/skills/expo-api-routes
    source_repo: expo/skills
    source_type: official
    date_added: 2026-07-01
    license: MIT
    license_source: https://github.com/expo/skills/blob/main/LICENSE
    ---
    
    ## 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
    
    ## 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.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related