wandres.dev
EL PRIMER WORKER · el fetch handler

Enrutar a mano: request.method y el pathname

Enrutar es, en su forma más pura, leer dos datos de la Request —el método y la ruta— y decidir con ellos qué Response fabricar. Construir un router a mano antes de confiar en cualquier framework.

⏱ 13 min

Un Worker responde lo mismo a cualquier petición… hasta que le enseñas a mirar. Enrutar es, en su forma más pura, leer dos datos de la Request —el método y la ruta— y decidir con ellos qué Response fabricar. Antes de tocar un solo framework de routing, vas a construir uno a mano, porque entender el mecanismo desnudo es lo que te permite luego confiar en las abstracciones.

🎯 Al terminar esta lección sabrás
  • Leer el método HTTP con request.method.
  • Extraer la ruta con new URL(request.url).pathname.
  • Combinar método y ruta en un router mínimo con switch.
  • Devolver un 404 por defecto para lo que no coincide.

Leer el método

Toda Request trae un método HTTP: GET, POST, PUT, DELETE. Lo lees directamente de la propiedad request.method:

export default {
  async fetch(request) {
    if (request.method !== "GET") {
      return new Response("Metodo no permitido", { status: 405 });
    }
    return new Response("Hola por GET");
  },
};

Fíjate en el segundo argumento de new Response: el objeto init, donde fijas el status. Un 405 es la respuesta correcta cuando la ruta existe pero el método no está permitido —un detalle que separa una API descuidada de una correcta. El método es una cadena en mayúsculas, así que compáralo siempre contra "GET", "POST" y demás, exactamente escritos.

Extraer la ruta

La URL entera está en request.url, pero como cadena de texto. Para trabajar con ella no la parsees a mano: usa el constructor URL del estándar web, que te da sus partes ya separadas.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    return new Response(`Has pedido la ruta: ${url.pathname}`);
  },
};

De ese objeto URL, la propiedad que gobierna el enrutado es pathname —la parte de la ruta, sin el dominio ni la query—. Para una petición a /api/users?id=7, el pathname es /api/users. También tienes ahí url.search y url.searchParams para leer los parámetros de la query sin esfuerzo:

const url = new URL(request.url);
const id = url.searchParams.get("id"); // "7"
💡
Nunca parsees URLs partiendo cadenas

La tentación de sacar la ruta con request.url.split o con expresiones regulares lleva a errores sutiles con las barras, la query o el encoding. El constructor URL está en el runtime, es el estándar y maneja todos esos casos por ti. new URL(request.url).pathname es la forma correcta, y punto.

Un router a mano

Con esas dos piezas —método y pathname— ya puedes construir un router. La forma más legible para pocas rutas es un switch sobre el pathname:

export default {
  async fetch(request) {
    const { pathname } = new URL(request.url);

    switch (pathname) {
      case "/":
        return new Response("Inicio");
      case "/salud":
        return Response.json({ estado: "ok" });
      case "/hora":
        return new Response(new Date().toISOString());
      default:
        return new Response("No encontrado", { status: 404 });
    }
  },
};

Observa dos cosas. Primera, Response.json(...) es un atajo del estándar para devolver JSON con el Content-Type correcto ya puesto. Segunda, el default del switch es el que devuelve el 404: si ninguna ruta coincide, la respuesta honesta es “no encontrado”.

flowchart TD
R[Request] --> P[Leer pathname y method]
P --> D[Comparar contra las rutas]
D -->|Coincide| H[Response de esa ruta]
D -->|Ninguna| E[Response 404]

Cuando cada ruta admite varios métodos, el patrón crece de forma natural: primero decides por pathname y, dentro, ramificas por request.method. Así separas “qué recurso” de “qué operación sobre él”, que es justo como piensa una API REST:

if (pathname === "/tareas") {
  if (request.method === "GET") return listarTareas();
  if (request.method === "POST") return crearTarea(request);
  return new Response("Metodo no permitido", { status: 405 });
}

Cuando una ruta recibe datos —un POST con JSON—, el handler lee el cuerpo de la Request con await request.json(), y ahí tienes ya el objeto para trabajar:

async function crearTarea(request: Request): Promise<Response> {
  const datos = await request.json();
  // ... guardar la tarea en algun binding ...
  return Response.json({ creada: true }, { status: 201 });
}

Enrutar, leer el método, parsear el cuerpo y devolver el estado correcto: son las cuatro operaciones con las que se construye cualquier API, y ya las tienes todas a mano.

Puestas en fila, son el esqueleto de cualquier endpoint:

  • Enrutarnew URL(request.url).pathname decide qué recurso.
  • Distinguir el métodorequest.method decide qué operación.
  • Leer la entradaawait request.json() o los searchParams.
  • Responder con intenciónResponse.json(...) con el status correcto.

Un framework te dará azúcar para las cuatro, pero ninguna es magia: todas nacen de la Request y terminan en una Response.

El 404 por defecto

Ese default no es un detalle menor: es una decisión de diseño. Un Worker siempre devuelve algo. Si no defines qué pasa con las rutas desconocidas, o bien devuelves un 404 explícito —lo correcto— o dejas un hueco por el que se cuela un comportamiento accidental. Enrutar a mano te obliga a hacer explícito el caso “ninguna de las anteriores”, que es justo el que se olvida en las APIs mal hechas. Y fíjate en la diferencia semántica que ya manejas: 404 es “esta ruta no existe”; 405 es “la ruta existe, pero no por este método”. Devolver el código correcto no es pedantería: es lo que un cliente HTTP necesita para saber qué hacer.

ℹ️
El pathname es sensible a los detalles

new URL(request.url).pathname te da la ruta tal cual llegó: /tareas y /tareas/ son cadenas distintas, y /Tareas no es /tareas. Un switch con casos exactos no perdona esas diferencias. Decide pronto tu política —por ejemplo, normalizar la barra final antes de comparar— para que tu router no falle por un detalle de escritura. Los frameworks resuelven esto por ti; a mano, es tu responsabilidad.

Todo framework de routing es azúcar sobre esto

Cuando dentro de unos niveles adoptes un framework como Hono, verás algo como registrar un handler para GET /users y parecerá magia. No lo es. Por dentro, ese framework hace exactamente lo que acabas de escribir a mano: lee request.method, parsea new URL(request.url).pathname, compara contra una tabla de rutas registradas —con soporte para patrones y parámetros como /users/:id, que un switch no te da— y, si nada coincide, devuelve un 404. La diferencia entre tu switch y un framework no es de naturaleza sino de escala y ergonomía: patrones de ruta, extracción de parámetros, middleware encadenado, grupos de rutas. Pero el átomo es idéntico. Por eso este ejercicio importa tanto: cuando entiendes que un router no es más que un gran condicional sobre método y ruta que termina fabricando una Response, dejas de tenerle miedo a la magia del framework y empiezas a saber exactamente qué hace por debajo —y, cuando falle, sabrás dónde mirar. Las abstracciones se disfrutan de verdad cuando podrías reescribirlas.

⚔️ Construye tu router
  1. Escribe un Worker con un switch sobre pathname que sirva /, /salud con Response.json, y /hora.
  2. Añade una comprobación de request.method: que /salud solo responda a GET y devuelva 405 en otro caso.
  3. Asegúrate de que el default devuelve un 404. Pruébalo pidiendo una ruta que no existe.
  4. Lee url.searchParams en alguna ruta y usa un parámetro de la query en la respuesta. Explica por qué usar URL es mejor que partir la cadena a mano.