La primera vez que publiqué una API JSON en
Cloudflare Workers,
la desplegué desde mi portátil a las 11 de la noche y estaba corriendo en más de 300 centros de datos antes de terminar mi té.
Sin Dockerfile, sin clúster de Kubernetes, sin drama de arranque en frío. Un único wrangler deploy
y un bundle de 1,2 KB. Esa experiencia es la razón por la que Workers se ha convertido en mi opción por defecto para servicios JSON-in, JSON-out —
webhooks, proxies, agregadores de API, autenticación en el borde. Si el 80% de tu backend es "parsear JSON, hacer una cosa y devolver JSON",
este es el artículo que me habría gustado tener cuando empecé.
Un Cloudflare Worker es básicamente una única función JavaScript (o TypeScript) que se ejecuta en isolates de V8 en el borde de Cloudflare. Recibe una Request, devuelve una Response, y tiene acceso a la API Fetch estándar. Si has usado fetch() en un navegador, ya conoces el 90% del runtime. Lo que no conoces es el pequeño conjunto de patrones que separan un Worker de juguete de uno que puedes ejecutar realmente en producción. De eso trata este artículo.
Tu primer endpoint JSON
Este es el endpoint JSON útil más pequeño. Devuelve un único objeto con un timestamp y un
mensaje. Guárdalo como src/index.ts en un proyecto de Wrangler:
export default {
async fetch(request, env, ctx) {
const payload = {
message: 'Hello from the edge',
servedAt: new Date().toISOString(),
colo: request.cf?.colo ?? 'unknown',
};
return Response.json(payload);
},
};Dos cosas a notar. Primero, Response.json() es un helper estático que serializa
el objeto y pone Content-Type: application/json por ti. No te fabriques tu propio
new Response(JSON.stringify(x)) a menos que necesites un content-type personalizado — simplemente
te olvidarás del encabezado eventualmente. Segundo, request.cf.colo te dice qué centro de datos
de Cloudflare está sirviendo la petición. Una petición desde Berlín mostrará FRA,
desde Tokio mostrará NRT. Todo el argumento del "edge" en un solo campo.
Parsear un cuerpo de petición JSON
Los endpoints POST necesitan leer un cuerpo. La API Fetch te da request.json(), que lee el stream del cuerpo y lo parsea en una sola llamada:
export default {
async fetch(request) {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const body = await request.json();
// body is now a regular JavaScript object
const { email, plan } = body;
return Response.json({
received: { email, plan },
ok: true,
});
},
};Parece limpio, pero este código tiene un bug que encontrarás a las 24 horas de publicarlo:
si el cliente envía un cuerpo vacío o JSON malformado, request.json() lanza
un SyntaxError, tu Worker se cae y Cloudflare devuelve un 500 genérico.
Esa no es la respuesta que quieres frente a los clientes.
Manejar JSON malformado — no dejes que devuelva 500
Siempre envuelve el parseo del cuerpo en un try/catch y devuelve un 400 apropiado. Este es el patrón que uso en todos los Workers:
async function readJson(request) {
try {
return { ok: true, data: await request.json() };
} catch (err) {
return {
ok: false,
error: 'Invalid JSON body',
detail: err.message,
};
}
}
export default {
async fetch(request) {
const result = await readJson(request);
if (!result.ok) {
return Response.json(result, { status: 400 });
}
const { email, plan } = result.data;
if (!email || !plan) {
return Response.json(
{ ok: false, error: 'email and plan are required' },
{ status: 422 },
);
}
return Response.json({ ok: true, email, plan });
},
};CORS para APIs JSON
Si tu Worker va a ser llamado desde un navegador en un origen diferente — que es
el caso normal — necesitas encabezados
CORS.
Los navegadores envían un preflight OPTIONS antes de la petición real para cualquier cosa
que sea más que un GET simple. Maneja ambos en un solo lugar:
const CORS_HEADERS = {
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
export default {
async fetch(request) {
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}
const data = { ping: 'pong', at: Date.now() };
return Response.json(data, { headers: CORS_HEADERS });
},
};Evita Access-Control-Allow-Origin: * en cualquier cosa que lea credenciales
o devuelva datos de usuario. Es uno de esos atajos que parece inofensivo en dev y se convierte en
un incidente de seguridad en prod. Hardcodea los orígenes que realmente sirves, o léelos desde una
allow-list en env.
Reenviar JSON a una API upstream
Uno de los usos más comunes de un Worker es como proxy fino: ocultar una clave de API, remodelar una respuesta, quitar campos que un cliente no necesita, o unir dos llamadas upstream en una. Este Worker llama a un servicio upstream, selecciona solo los campos que nos interesan, y devuelve un payload JSON más limpio:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const userId = url.searchParams.get('id');
if (!userId) {
return Response.json({ error: 'id required' }, { status: 400 });
}
const upstream = await fetch(
`https://api.internal.example.com/users/${userId}`,
{
headers: { 'Authorization': `Bearer ${env.UPSTREAM_TOKEN}` },
},
);
if (!upstream.ok) {
return Response.json(
{ error: 'upstream failed', status: upstream.status },
{ status: 502 },
);
}
const full = await upstream.json();
// Strip internal fields before returning to the client
const safe = {
id: full.id,
displayName: full.display_name,
avatarUrl: full.avatar_url,
joinedAt: full.created_at,
};
return Response.json(safe);
},
};Dos cosas a tener en cuenta. Primero, siempre comprueba upstream.ok
antes de llamar a .json() — un 500 del upstream tendrá HTML o una página de error,
y llamar a .json() sobre eso lanza igual que con cualquier otro JSON malformado.
Segundo, guarda los secretos como UPSTREAM_TOKEN en los secrets de Wrangler
(wrangler secret put UPSTREAM_TOKEN) — nunca en wrangler.toml
y nunca commiteados a git.
Cachear respuestas JSON en el borde
Cuando un upstream es lento o caro, la Cache API te permite memoizar JSON en el borde. Cada centro de datos mantiene su propia caché, así que el primer usuario en Frankfurt paga el coste upstream, y los siguientes 10.000 lo obtienen en menos de 5ms desde la RAM cercana:
export default {
async fetch(request, env, ctx) {
const cache = caches.default;
const cacheKey = new Request(request.url, request);
let response = await cache.match(cacheKey);
if (response) {
return response;
}
const upstream = await fetch('https://api.example.com/popular-items');
const data = await upstream.json();
response = Response.json(data, {
headers: {
'Cache-Control': 'public, max-age=60',
},
});
// Don't block the response on the cache write
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
},
};El ctx.waitUntil() es la parte no obvia. Sin él, el
cache.put() se espera y tu respuesta aguarda una escritura a disco que no
necesita importarle. waitUntil te permite devolver la respuesta inmediatamente
mientras el runtime mantiene viva la escritura de caché en segundo plano. Es el mismo patrón
que usarías para beacons de analíticas, forwarding de logs, cualquier cosa fire-and-forget.
Desarrollo local con Wrangler
No necesitas una cuenta de Cloudflare para iterar. Instala Wrangler, crea un proyecto, y obtienes un runtime local de Workers que se parece mucho al de producción:
npm create cloudflare@latest my-json-api
cd my-json-api
npm run dev
# Worker is now live at http://localhost:8787
# Hit it from another terminal:
curl -X POST http://localhost:8787 \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","plan":"pro"}'El runtime local usa workerd, el mismo motor que Cloudflare ejecuta en producción.
Los comportamientos que difieren (latencia de KV, semántica de caché, campos request.cf)
están bien documentados y raramente te muerden para APIs JSON simples. Despliega con
wrangler deploy y el mismo código está en vivo globalmente en segundos.
Herramientas útiles para construir APIs JSON con Workers
Unas cuantas herramientas a las que recurro constantemente cuando construyo Workers que manejan JSON: JSON Formatter para formatear una respuesta upstream fea que intento hacer ingeniería inversa, JSON Validator cuando un payload POST falla y necesito saber exactamente dónde, JSON Path para planificar la lógica de selección de campos antes de escribirla, y JSON Minifier cuando quiero comprobar si el tamaño en el cable realmente importa para un endpoint dado.
El formato JSON en sí está especificado en
RFC 8259
— vale la pena echarle un vistazo si alguna vez te topas con un caso límite tipo "¿mi parser permite NaN?"
(respuesta: no debería). La propia
galería de ejemplos de Workers de Cloudflare
tiene recetas para verificación de JWT, tests A/B, reescritura de HTML y una docena de otros patrones
una vez que hayas superado los fundamentos de este artículo.
Para cerrar
Cloudflare Workers encaja muy bien para APIs JSON — pequeño, rápido, distribuido
globalmente, y lo suficientemente barato como para dejar corriendo proyectos personales. El camino feliz es
solo request.json() y Response.json(), pero el camino de producción
implica cuatro hábitos extra: envolver el parseo del cuerpo en try/catch, añadir encabezados CORS intencionadamente,
comprobar upstream.ok antes de parsear respuestas proxeadas, y usar
ctx.waitUntil para escrituras de caché y otro trabajo en segundo plano. Haz bien esos cuatro
y publicarás Workers que se mantienen en pie.