Generated banner for Expo API Routes
Generated logo for Expo API Routes

Expo API Routes

OpenAIOfficialCreated by Expo TeamDistributed by OpenAI

  • Skill
  • Tool integration
  • Software development
  • MIT
  • Low risk

From the author

Guidelines for creating API routes in Expo Router with EAS Hosting
— the description by Expo Team

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a comprehensive guide for implementing backend API routes directly within an Expo project using Expo Router. It covers the file-based routing structure, request handling, environment variable management for secrets, and deployment workflows for EAS Hosting. The guide specifically addresses the constraints of the Cloudflare Workers runtime, offering best practices for database connectivity, error handling, and security.

What it does

  • Defines the file-based structure for creating API routes in Expo Router.
  • Demonstrates how to handle HTTP methods, query parameters, and JSON request bodies.
  • Explains how to securely manage server-side secrets using environment variables.
  • Provides patterns for proxying external APIs and implementing authentication middleware.
  • Outlines the deployment process to EAS Hosting and Cloudflare Workers.
  • Offers guidance on selecting compatible cloud databases for edge environments.

When to use it

  • When you need to hide sensitive API keys or database credentials from the client.
  • When performing server-side data validation or processing.
  • When you need to proxy requests to third-party services.
  • When you need to offload heavy computation from the mobile client.
  • When you need to implement webhook endpoints for external services.

Advantages

  • Keeps backend logic and frontend code within the same repository.
  • Provides a secure way to handle sensitive operations away from the client.
  • Leverages edge-based execution for improved performance.
  • Simplifies the development workflow by using standard Web APIs.
  • Enables easy deployment via the Expo ecosystem.

Usage examples

“How do I create a POST endpoint in Expo Router to handle user registration?”
The agent provides a code snippet showing how to create a file named users+api.ts, export an async POST function, parse the request body, and return a JSON response.
“How can I securely call the OpenAI API from my Expo app?”
The agent explains how to create an API route that uses process.env.OPENAI_API_KEY to fetch data from OpenAI, ensuring the key is never exposed to the client.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/openai/expo-api-routes/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip expo-api-routes.zip -d .agents/skills

Documentation by Expo Team

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

Files

ATTRIBUTION.md

Attribution

This folder is an unmodified copy of the Agent Skill "expo-api-routes", distributed by AutoGineer (https://autogineer.com/agent-skills).

The skill's own files are unmodified copies. AutoGineer is not affiliated with or endorsed by the author or the publisher.

Files added by AutoGineer

  • ATTRIBUTION.md: this file, written by AutoGineer

  • LICENSE: copied from plugins/expo/LICENSE

Requirements and compatibility

Written for: OpenAI Codex

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs network access, Needs an API key, Needs Node.js

Security

What our scanner noticed· Low risk

  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Expo Team
Publisher
OpenAI (Official)
Distributed by
OpenAI
Source
openai/plugins · plugins/expo/skills/expo-api-routes · commit d420d13
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from plugins/expo/LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Expo Team or OpenAI.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download expo-api-routes.zip