EAS Hosting
Servicio EAS: se aplican costos. EAS Hosting es un producto pago de Expo Application Services con límites de nivel gratuito; los despliegues de producción consumen la cuota de solicitudes y ancho de banda de tu plan. Consulta https://expo.dev/pricing. La creación de rutas API y la exportación del bundle web son gratuitas y de código abierto, y puedes auto-alojar la salida del servidor exportado en lugar de usar EAS Hosting.
EAS Hosting despliega tu app web de Expo y rutas API en el edge administrado de Expo (Cloudflare Workers). Exporta el bundle web con npx expo export -p web y envíalo con eas deploy: el mismo comando despliega cualquier ruta API de Expo Router incluida. Esta skill cubre el despliegue de un sitio web, la creación de rutas API y el runtime de hosting; consulta la sección de Despliegue más abajo para el flujo de trabajo.
Cuándo Usar Rutas API
Usa rutas API cuando necesites:
- Secretos del lado del servidor — Claves de API, credenciales de base de datos o tokens que nunca deben llegar al cliente
- Operaciones de base de datos — Consultas directas a la base de datos que no deben exponerse
- Proxies de APIs de terceros — Ocultar claves de API al llamar a servicios externos (OpenAI, Stripe, etc.)
- Validación del lado del servidor — Validar datos antes de escribir en la base de datos
- Endpoints de webhook — Recibir callbacks de servicios como Stripe o GitHub
- Límite de tasa — Controlar el acceso a nivel de servidor
- Cómputo pesado — Descargar procesamiento que sería lento en dispositivos móviles
Cuándo NO Usar Rutas API
Evita las rutas API cuando:
- Los datos ya son públicos — Usa fetch directo a APIs públicas en su lugar
- No se requieren secretos — Datos estáticos u operaciones seguras para el cliente
- Se necesitan actualizaciones en tiempo real — Usa WebSockets o servicios como Supabase Realtime
- CRUD simple — Considera Firebase, Supabase o Convex para backends administrados
- Subida de archivos — Usa subidas directas a almacenamiento (URLs prefirmadas de S3, Cloudflare R2)
- Solo autenticación — Usa Clerk, Auth0 o Firebase Auth en su lugar
Estructura de Archivos
Las rutas API viven en el directorio app con el sufijo +api.ts:
app/
api/
hello+api.ts → GET /api/hello
users+api.ts → /api/users
users/[id]+api.ts → /api/users/:id
(tabs)/
index.tsx
Ruta API Básica
// app/api/hello+api.ts
export function GET(request: Request) {
return Response.json({ message: "Hello from Expo!" });
}
Métodos HTTP
Exporta funciones con nombre para cada método HTTP:
// 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 });
}
Rutas Dinámicas
// app/api/users/[id]+api.ts
export function GET(request: Request, { id }: { id: string }) {
return Response.json({ userId: id });
}
Manejo de Solicitudes
Parámetros de Consulta
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 });
}
Cabeceras
export function GET(request: Request) {
const auth = request.headers.get("Authorization");
if (!auth) {
return Response.json({ error: "No autorizado" }, { status: 401 });
}
return Response.json({ authenticated: true });
}
Cuerpo JSON
export async function POST(request: Request) {
const { email, password } = await request.json();
if (!email || !password) {
return Response.json({ error: "Campos faltantes" }, { status: 400 });
}
return Response.json({ success: true });
}
Variables de Entorno
Usa process.env para secretos del lado del servidor:
// 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);
}
Configura las variables de entorno:
- Local: Crea un archivo
.env(nunca lo subas al repositorio) - EAS Hosting: Usa
eas env:createo el panel de Expo
Cabeceras CORS
Añade CORS para clientes web:
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 });
}
Manejo de Errores
export async function POST(request: Request) {
try {
const body = await request.json();
// Procesar...
return Response.json({ success: true });
} catch (error) {
console.error("Error de API:", error);
return Response.json({ error: "Error interno del servidor" }, { status: 500 });
}
}
Pruebas Locales
Inicia el servidor de desarrollo con las rutas API:
npx expo serve
Esto inicia un servidor local en http://localhost:8081 con soporte completo para rutas API.
Prueba con curl:
curl http://localhost:8081/api/hello
curl -X POST http://localhost:8081/api/users -H "Content-Type: application/json" -d '{"name":"Test"}'
Despliegue en EAS Hosting
Prerrequisitos
npm install -g eas-cli
eas login
Desplegar
El despliegue envía tu bundle web y cualquier ruta API de Expo Router juntos: eas deploy maneja ambos. La exportación se ejecuta tanto si tienes un sitio web completo, un backend solo con rutas API, o ambos.
# Exportar el bundle web (incluye cualquier ruta API)
npx expo export -p web
# Desplegar una vista previa (URL estilo PR)
npx eas-cli@latest deploy
# Desplegar a producción
npx eas-cli@latest deploy --prod
Todo termina en EAS Hosting (Cloudflare Workers).
Variables de Entorno para Producción
# Crear un secreto
eas env:create --name OPENAI_API_KEY --value sk-xxx --environment production
# O usar el panel de Expo
Dominio Personalizado
Configúralo en eas.json o en el panel de Expo.
Automatizar con EAS Workflows
Despliega el sitio web (y las rutas API) en cada push a main con un flujo de trabajo type: deploy:
.eas/workflows/deploy.yml
name: Deploy
on:
push:
branches:
- main
# https://docs.expo.dev/eas/workflows/syntax/#deploy
jobs:
deploy_web:
type: deploy
params:
prod: true
Los despliegues de vista previa para pull requests usan el mismo tipo de trabajo con prod: false:
name: Web PR Preview
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
type: deploy
params:
prod: false
Para crear o validar YAML de flujos de trabajo más allá de estos ejemplos, usa la skill eas-workflows.
Runtime de EAS Hosting (Cloudflare Workers)
Las rutas API se ejecutan en Cloudflare Workers. Limitaciones clave:
APIs Faltantes o Limitadas
- Sin sistema de archivos de Node.js — El módulo
fsno está disponible - Sin módulos nativos de Node — Usa Web APIs o polyfills
- Tiempo de ejecución limitado — 30 segundos de tiempo de espera para tareas intensivas en CPU
- Sin conexiones persistentes — WebSockets requieren Durable Objects
- fetch está disponible — Usa fetch estándar para solicitudes HTTP
Usa Web APIs en su Lugar
// Usa Web Crypto en lugar de crypto de Node
const hash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode("data")
);
// Usa fetch en lugar de node-fetch
const response = await fetch("https://api.example.com");
// Usa Response/Request (ya disponibles)
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
Opciones de Base de Datos
Dado que el sistema de archivos no está disponible, usa bases de datos en la nube:
- Cloudflare D1 — SQLite en el edge
- Turso — SQLite distribuido
- PlanetScale — MySQL sin servidor
- Supabase — Postgres con API REST
- Neon — Postgres sin servidor
Ejemplo con 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);
}
Llamar a Rutas API desde el Cliente
// Desde componentes React Native
const response = await fetch("/api/hello");
const data = await response.json();
// Con cuerpo
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John" }),
});
Patrones Comunes
Middleware de Autenticación
// 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: "No autorizado" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Verificar 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 de API Externa
// 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());
}
Reglas
- NUNCA expongas claves de API o secretos en el código del cliente
- SIEMPRE valida y sanitiza la entrada del usuario
- Usa códigos de estado HTTP adecuados (200, 201, 400, 401, 404, 500)
- Maneja errores con gracia usando try/catch
- Mantén las rutas API enfocadas — una responsabilidad por endpoint
- Usa TypeScript para seguridad de tipos
- Registra errores del lado del servidor para depuración
Enviar Comentarios
Si encuentras errores, información engañosa o desactualizada en esta skill, repórtala para que Expo pueda mejorar:
npx --yes @expo/agent-cli@latest feedback --category skills --subject "eas-hosting" "<comentario procesable>"
Envía solo cuando tengas algo específico y procesable que informar. Incluye todo el contexto relevante posible.
Si un agente falla repetidamente en una tarea de Expo o el usuario tiene que intervenir, envía un breve informe de la tarea, el resultado esperado y lo que salió mal usando --category evals y el paquete o comando de Expo relevante como --subject.