expo-api-routes — скилл для Codex: API-роуты в Expo Router
Скилл объясняет агенту, как писать серверные API-роуты в Expo Router и выкладывать их на EAS Hosting. Пригодится, когда ключи и запросы к базе нельзя держать на клиенте.
npx skills add openai/plugins --skill expo-api-routes -a codexЧто делает expo-api-routes
Скилл задаёт правила для файлов с суффиксом +api.ts в каталоге app. Каждый HTTP-метод — отдельная именованная функция: GET, POST, PUT, DELETE. Динамические сегменты вроде app/api/users/[id]+api.ts агент разберёт по образцу из скилла.
Внутри есть готовые примеры: чтение query-параметров через URL, проверка заголовка Authorization, разбор JSON-тела, CORS-заголовки и обработка ошибок с кодом 500. Секреты берутся из process.env, локально — через файл .env, в продакшене — командой eas env:create. Локальная проверка идёт через npx expo serve, деплой — через eas deploy.
Роуты исполняются на Cloudflare Workers, поэтому модуль fs и нативные модули Node там недоступны, а на CPU-задачу отводится 30 секунд. Для хешей скилл советует Web Crypto, для HTTP — обычный fetch, для базы — облачные варианты вроде Turso или Cloudflare D1. Если в проекте уже есть прямое подключение к базе из клиента, это повод проверить, не утекают ли креды.
Какие задачи решает
- спрятать ключи OpenAI и Stripe от клиента
- принимать вебхуки от Stripe или GitHub
- провалидировать данные перед записью в базу
- подключить облачную базу к роутам на Cloudflare Workers
- ограничить доступ к эндпоинту на стороне сервера
Примеры запросов агенту
- Создай API-роут в app/api/hello+api.ts, который возвращает JSON с приветствием
- Добавь POST-обработчик в app/api/items+api.ts с проверкой полей и кодом 400 при пропуске
- Сделай динамический роут app/api/users/[id]+api.ts и достань id из параметров
Как установить expo-api-routes
1Через Skills CLI (Vercel)
npx skills add openai/plugins --skill expo-api-routes -a codex2Вручную: положить файлы
.agents/skills/expo-api-routes/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add openai/plugins --skill expo-api-routes -a claude-code2Вручную: положить файлы
~/.claude/skills/expo-api-routes/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/expo-api-routes/
Вручную: положить файлы
.cursor/skills/expo-api-routes/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install openai/plugins expo-api-routes2Вручную: положить файлы
.github/skills/expo-api-routes/SKILL.mdКомандой платформы
gemini skills install https://github.com/openai/plugins --path plugins/expo/skills/expo-api-routesВручную: положить файлы
.devin/skills/expo-api-routes/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Claude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Скиллы Codex: как установить, где лежат и как вызвать
Из того же набора304
Весь набор →Похожие
Рядом в теме «Бэкенд и API»
Всё по теме «Бэкенд и API» →Вопросы о expo-api-routes
Как установить expo-api-routes в Claude?
Выполните npx skills add openai/plugins --skill expo-api-routes -a codex. После этого файл скилла появится по пути .agents/skills/expo-api-routes/SKILL.md.
Что делает expo-api-routes?
Даёт агенту правила создания API-роутов в Expo Router: структуру файлов +api.ts, обработку методов, заголовков и тела запроса, работу с секретами и деплой на EAS Hosting.
Что нужно для работы скилла?
Для локального запуска роутов — npx expo serve, для деплоя — установленный eas-cli и вход через eas login. Секреты в продакшене задаются командой eas env:create.
Исходный текст
SKILL.md · MIT
---
name: expo-api-routes
description: Guidelines for creating API routes in Expo Router with EAS Hosting
version: 1.0.0
license: MIT
---
## 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
© Expo Team · MIT · openai/plugins