npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jamx-framework/router

v1.0.1

Published

JAMX Framework — File-based Router

Downloads

115

Readme

@jamx-framework/router

Descripción

Sistema de enrutamiento basado en archivos para JAMX Framework. Convierte automáticamente la estructura de archivos del proyecto en rutas HTTP (API) y páginas (SSR), escaneando el sistema de archivos para descubrir handlers y componentes. Proporciona matching de rutas con parámetros dinámicos, wildcards, y separación clara entre API endpoints y páginas web.

Cómo funciona

El router opera en tres fases:

  1. Escaneo: FileScanner recorre el proyecto buscando archivos *.handler.ts (API) y *.page.tsx (páginas)
  2. Parseo: RouteParser convierte nombres de archivos y carpetas en definiciones de rutas con parámetros
  3. Matching: RouteMatcher empareja URLs entrantes con rutas registradas, extrayendo parámetros
  4. Ejecución: ApiHandlerExecutor o PageHandlerExecutor ejecutan el handler correspondiente

Componentes principales

FileRouter (src/router.ts)

Clase principal que implementa RequestDispatcher:

  • initialize(): Escanea el proyecto y construye la tabla de rutas
  • dispatch(req, res): Despacha una request al handler correcto
  • getRoutes(): Retorna todas las rutas registradas (para compilador)
  • invalidate(filePath): Re-escanear proyecto (HMR)

FileScanner (src/scanner/file-scanner.ts)

Escanea el sistema de archivos para descubrir rutas:

  • Busca archivos *.handler.ts en src/api/
  • Busca archivos *.page.tsx en src/pages/
  • Construye objetos Route con path, kind, filePath

RouteParser (src/scanner/route-parser.ts)

Convierte paths de archivos en patrones de ruta:

  • /api/users.handler.ts/api/users (API)
  • /pages/users/[id].page.tsx/users/:id (página con param)
  • /pages/admin/dashboard.page.tsx/admin/dashboard

RouteMatcher (src/matcher/matcher.ts)

Empareja URLs con rutas registradas:

  • Matching exacto para estáticos
  • Extracción de parámetros (:id, :slug)
  • Soporte para wildcards (*)

ApiHandlerExecutor (src/handler/api-handler.ts)

Ejecuta handlers de API:

  • Carga el módulo dinámicamente
  • Invoca el método HTTP correcto (GET, POST, etc.)
  • Pasa req y res de JAMX

PageHandlerExecutor (src/handler/page-handler.ts)

Ejecuta componentes de página:

  • Carga el módulo dinámicamente
  • Usa SSRRenderer para renderizar a HTML
  • Retorna respuesta con HTML completo

Convenciones de archivos

API Handlers

Ubicación: src/api/

Patrón: *.handler.ts

Ejemplos:

src/api/users.handler.ts        → GET /api/users
src/api/users/[id].handler.ts  → GET /api/users/:id
src/api/auth/login.handler.ts  → POST /api/auth/login

Estructura del módulo:

// src/api/users.handler.ts
export default {
  GET: async (req, res) => {
    res.json({ users: [] });
  },
  POST: async (req, res) => {
    const user = req.body;
    res.created(user);
  },
};

Páginas

Ubicación: src/pages/

Patrón: *.page.tsx

Ejemplos:

src/pages/index.page.tsx        → /
src/pages/about.page.tsx        → /about
src/pages/users/[id].page.tsx   → /users/:id
src/pages/blog/[slug]/[id].page.tsx → /blog/:slug/:id

Estructura del módulo:

// src/pages/index.page.tsx
import { jsx } from '@jamx-framework/renderer';

export default {
  render(ctx) {
    return jsx('div', {}, 'Página de inicio');
  },
  meta(ctx) {
    return {
      title: 'Inicio - Mi App',
      description: 'Descripción de la página',
    };
  },
};

Uso básico

Configurar el router en el servidor

import { createServer } from '@jamx-framework/server';
import { FileRouter } from '@jamx-framework/router';

const server = await createServer();

// Configurar router file-based
const router = new FileRouter({ projectRoot: './' });
await router.initialize();

// Usar router como middleware
server.use(router.dispatch.bind(router));

await server.listen({ port: 3000 });

Estructura de proyecto

my-app/
├── jamx.config.ts
├── src/
│   ├── api/
│   │   ├── users.handler.ts
│   │   └── users/[id].handler.ts
│   └── pages/
│       ├── index.page.tsx
│       ├── about.page.tsx
│       └── users/[id].page.tsx
└── package.json

Definir API endpoints

// src/api/users.handler.ts
export default {
  GET: async (req, res) => {
    const users = await db.users.findAll();
    res.json(users);
  },

  POST: async (req, res) => {
    const user = await db.users.create(req.body);
    res.created(user);
  },
};
// src/api/users/[id].handler.ts
export default {
  GET: async (req, res) => {
    const id = req.params.id;
    const user = await db.users.findById(id);
    if (!user) {
      res.notFound('User not found');
      return;
    }
    res.json(user);
  },

  PUT: async (req, res) => {
    const id = req.params.id;
    const user = await db.users.update(id, req.body);
    res.json(user);
  },

  DELETE: async (req, res) => {
    const id = req.params.id;
    await db.users.delete(id);
    res.noContent();
  },
};

Definir páginas SSR

// src/pages/index.page.tsx
import { jsx } from '@jamx-framework/renderer';

export default {
  render(ctx) {
    return jsx('div', { class: 'home' }, [
      jsx('h1', {}, 'Bienvenido a mi app'),
      jsx('p', {}, 'Esta es la página de inicio'),
    ]);
  },

  meta(ctx) {
    return {
      title: 'Inicio',
      description: 'Página de inicio de mi aplicación JAMX',
    };
  },
};
// src/pages/users/[id].page.tsx
import { jsx } from '@jamx-framework/renderer';

export default {
  async render(ctx) {
    const id = ctx.params.id;
    const user = await db.users.findById(id);

    if (!user) {
      return jsx('div', {}, [
        jsx('h1', {}, 'Usuario no encontrado'),
      ]);
    }

    return jsx('div', { class: 'user-profile' }, [
      jsx('h1', {}, user.name),
      jsx('p', {}, `Email: ${user.email}`),
    ]);
  },

  meta(ctx) {
    return {
      title: 'Perfil de usuario',
    };
  },
};

Layouts compartidos

// src/pages/_layout.page.tsx
import { jsx, Fragment } from '@jamx-framework/renderer';

export default {
  render({ children, ctx }) {
    return jsx('html', {}, [
      jsx('head', {}, [
        jsx('title', {}, ctx.meta?.title ?? 'Mi App'),
        jsx('meta', {
          name: 'description',
          content: ctx.meta?.description ?? '',
        }),
        jsx('link', { rel: 'stylesheet', href: '/styles.css' }),
      ]),
      jsx('body', {}, [
        jsx('header', {}, jsx('nav', {}, 'Navegación')),
        jsx('main', {}, children),
        jsx('footer', {}, '© 2024'),
      ]),
    ]);
  },
};

API Reference

FileRouter

Constructor

new FileRouter(options: RouterOptions)

RouterOptions:

interface RouterOptions {
  projectRoot: string;  // Ruta raíz del proyecto
}

Métodos

initialize()
async initialize(): Promise<void>

Escanea el proyecto y construye la tabla de rutas. Debe llamarse antes de dispatch().

dispatch()
async dispatch(req: JamxRequest, res: JamxResponse): Promise<void>

Despacha una request al handler correspondiente. Implementa RequestDispatcher.

getRoutes()
getRoutes(): Route[]

Retorna todas las rutas registradas. Usado por el compilador para generar AppRoutes.

invalidate()
async invalidate(filePath: string): Promise<void>

Invalida la cache y re-escanea el proyecto. Usado por HMR (Hot Module Replacement).

Tipos

Route

interface Route {
  path: string;           // Path con parámetros, ej: /api/users/:id
  kind: RouteKind;        // "page" | "api"
  methods: HttpMethod[];  // Métodos HTTP para API, [] para páginas
  filePath: string;       // Ruta absoluta al archivo
  segments: RouteSegment[]; // Segmentos parseados del path
}

RouteSegment

type RouteSegment =
  | { kind: "static"; value: string }  // Segmento estático como "users"
  | { kind: "param"; name: string }    // Parámetro como ":id"
  | { kind: "wildcard" };              // Wildcard "/*"

RouteMatch

interface RouteMatch {
  route: Route;
  params: Record<string, string>;  // Parámetros extraídos, ej: { id: "123" }
}

HttpMethod

type HttpMethod =
  | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";

RouteKind

type RouteKind = "page" | "api";

ApiHandlerModule

interface ApiHandlerModule {
  GET?: ApiHandlerFn;
  POST?: ApiHandlerFn;
  PUT?: ApiHandlerFn;
  PATCH?: ApiHandlerFn;
  DELETE?: ApiHandlerFn;
  OPTIONS?: ApiHandlerFn;
  HEAD?: ApiHandlerFn;
  default?: ApiHandlerFn;  // Handler para métodos no definidos
}

ApiHandlerFn

type ApiHandlerFn = (
  req: JamxRequest,
  res: JamxResponse
) => Promise<void> | void;

PageModule

interface PageModule {
  default: PageComponent;
}

PageComponent

interface PageComponent {
  render: (ctx: RenderContext) => JamxNode;
  meta?: (ctx: RenderContext) => PageMeta;
  layout?: unknown;  // Layout component (por implementar)
}

PageMeta

interface PageMeta {
  title?: string;
  description?: string;
  [key: string]: unknown;
}

AppRoutesBase

type AppRoutesBase = Record<string, RouteDefinition>;

Tipo base para AppRoutes generado por el compilador.

FileScanner

scanProject()

async scanProject(projectRoot: string): Promise<Route[]>

Escanea el proyecto y retorna todas las rutas encontradas.

Lógica de escaneo:

  1. Busca src/api/**/*.handler.ts → rutas API
  2. Busca src/pages/**/*.page.tsx → rutas de página
  3. Convierte paths a patrones de ruta
  4. Retorna array de Route

Ejemplo:

src/api/users.handler.ts → { path: '/api/users', kind: 'api', methods: ['GET', 'POST'] }
src/pages/about.page.tsx → { path: '/about', kind: 'page', methods: [] }

RouteParser

parseRoute()

parseRoute(filePath: string, kind: RouteKind): Route

Convierte un path de archivo en una definición de ruta.

Reglas:

  • api/ → prefijo /api
  • pages/ → sin prefijo (rutas públicas)
  • [param].handler/page.tsx:param
  • *.handler.ts → elimina extensión
  • *.page.tsx → elimina extensión

Ejemplos:

src/api/users.handler.ts           → /api/users
src/api/users/[id].handler.ts     → /api/users/:id
src/pages/blog/[slug]/[id].page.tsx → /blog/:slug/:id
src/pages/admin/dashboard.page.tsx → /admin/dashboard

RouteMatcher

setRoutes()

setRoutes(routes: Route[]): void

Carga las rutas para matching.

match()

match(path: string, method: HttpMethod): RouteMatch | null

Busca una ruta que coincida con el path y método.

Algoritmo:

  1. Iterar rutas en orden de registro
  2. Para cada ruta, comparar segmento por segmento
  3. Si encuentra :param, extraer valor
  4. Si encuentra *, hacer match con cualquier resto
  5. Si todos los segmentos coinciden, retornar RouteMatch
  6. Si no hay match, retornar null

Ejemplo:

matcher.setRoutes([
  { path: '/api/users', kind: 'api', methods: ['GET'], ... },
  { path: '/api/users/:id', kind: 'api', methods: ['GET'], ... },
]);

matcher.match('/api/users/123', 'GET');
// → { route: {...}, params: { id: '123' } }

ApiHandlerExecutor

execute()

async execute(route: Route, req: JamxRequest, res: JamxResponse): Promise<void>

Ejecuta el handler de API correspondiente.

Proceso:

  1. Cargar módulo desde route.filePath
  2. Obtener handler por método HTTP (route.methods + default)
  3. Si no existe handler para el método → 405
  4. Invocar handler con req y res
  5. Manejar errores y enviar respuesta

PageHandlerExecutor

execute()

async execute(route: Route, req: JamxRequest, res: JamxResponse): Promise<void>

Ejecuta el componente de página y envía HTML.

Proceso:

  1. Cargar módulo desde route.filePath
  2. Obtener componente default export
  3. Crear RenderContext desde req
  4. Invocar SSRRenderer.render(component, ctx)
  5. Enviar HTML en res.body con headers apropiados

Flujo interno detallado

1. Inicialización

const router = new FileRouter({ projectRoot: './' });
await router.initialize();

** Dentro de initialize():**

async initialize() {
  // 1. Escanear proyecto
  this.routes = await this.scanner.scanProject(this.options.projectRoot);

  // 2. Configurar matcher
  this.matcher.setRoutes(this.routes);

  this.initialized = true;
}

FileScanner.scanProject():

async scanProject(root) {
  const routes = [];

  // Buscar API handlers
  const apiFiles = glob(`${root}/src/api/**/*.handler.ts`);
  for (const file of apiFiles) {
    const route = this.parseHandler(file, root);
    routes.push(route);
  }

  // Buscar páginas
  const pageFiles = glob(`${root}/src/pages/**/*.page.tsx`);
  for (const file of pageFiles) {
    const route = this.parsePage(file, root);
    routes.push(route);
  }

  return routes;
}

2. Despacho de request

await router.dispatch(req, res);

** Dentro de dispatch():**

async dispatch(req, res) {
  // Auto-inicializar si es necesario
  if (!this.initialized) {
    await this.initialize();
  }

  // Parsear query string
  const qIndex = req.url.indexOf("?");
  if (qIndex !== -1) {
    req.query = qs.parse(req.url.slice(qIndex + 1));
  }

  // Buscar ruta
  const match = this.matcher.match(req.path, req.method);

  if (!match) {
    throw new NotFoundException(
      `Cannot ${req.method} ${req.path}`,
      "ROUTE_NOT_FOUND"
    );
  }

  // Inyectar params
  req.params = match.params;

  // Ejecutar handler
  if (match.route.kind === "api") {
    await this.apiHandler.execute(match.route, req, res);
  } else {
    await this.pageHandler.execute(match.route, req, res);
  }
}

3. Matching de rutas

// Ejemplo de rutas
const routes = [
  { path: '/api/users', segments: [{static: 'api'}, {static: 'users'}] },
  { path: '/api/users/:id', segments: [{static: 'api'}, {static: 'users'}, {param: 'id'}] },
];

// Request: GET /api/users/123
match = matcher.match('/api/users/123', 'GET');
// → { route: routes[1], params: { id: '123' } }

Algoritmo de RouteMatcher.match():

match(path, method) {
  const pathSegments = path.split('/').filter(Boolean);

  for (const route of this.routes) {
    // Verificar método para APIs
    if (route.kind === 'api' && !route.methods.includes(method)) {
      continue;
    }

    // Comparar segmentos
    const params = {};
    let matched = true;

    for (let i = 0; i < route.segments.length; i++) {
      const segment = route.segments[i];
      const pathSegment = pathSegments[i];

      if (segment.kind === 'static') {
        if (segment.value !== pathSegment) {
          matched = false;
          break;
        }
      } else if (segment.kind === 'param') {
        params[segment.name] = pathSegment;
      } else if (segment.kind === 'wildcard') {
        // Wildcard captura el resto
        params[segment.name || 'wildcard'] = pathSegments.slice(i).join('/');
        break;
      }
    }

    if (matched && pathSegments.length === route.segments.length) {
      return { route, params };
    }
  }

  return null;
}

4. Ejecución de API handler

// src/api/users.handler.ts
export default {
  GET: async (req, res) => {
    res.json({ users: [] });
  },
};

ApiHandlerExecutor.execute():

async execute(route, req, res) {
  // Cargar módulo dinámicamente
  const mod = await import(route.filePath);
  const handler = mod.default;

  // Buscar método específico
  let fn = handler[req.method];
  if (!fn) {
    fn = handler.default;
  }

  if (!fn) {
    res.json({ error: 'Method Not Allowed' }, 405);
    return;
  }

  // Ejecutar
  await fn(req, res);
}

5. Ejecución de página

// src/pages/index.page.tsx
export default {
  render(ctx) {
    return jsx('div', {}, 'Hello');
  },
};

PageHandlerExecutor.execute():

async execute(route, req, res) {
  // Cargar módulo
  const mod = await import(route.filePath);
  const page = mod.default;

  // Crear contexto de renderizado
  const ctx: RenderContext = {
    env: process.env.NODE_ENV,
    path: req.path,
    url: req.url,
    params: req.params,
    locals: req.locals,
  };

  // Renderizar con SSRRenderer
  const result = await this.renderer.render(page, ctx);

  // Enviar respuesta
  res.status(result.statusCode);
  for (const [key, value] of Object.entries(result.headers)) {
    res.header(key, value);
  }
  res.send(result.html);
}

Configuración

tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist",
    "tsBuildInfoFile": "dist/.tsbuildinfo"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

Scripts disponibles

  • pnpm build - Compila TypeScript a JavaScript
  • pnpm dev - Compilación en watch mode
  • pnpm test - Ejecuta tests unitarios
  • pnpm test:watch - Tests en watch mode
  • pnpm type-check - Verifica tipos sin compilar
  • pnpm clean - Limpia archivos compilados

Testing

Tests unitarios

import { FileRouter } from '@jamx-framework/router';
import { describe, it, expect, beforeEach } from 'vitest';

describe('Router', () => {
  let router: FileRouter;

  beforeEach(async () => {
    router = new FileRouter({ projectRoot: './test-fixtures' });
    await router.initialize();
  });

  it('should match API routes', () => {
    const routes = router.getRoutes();
    const apiRoute = routes.find(r => r.path === '/api/users');
    expect(apiRoute).toBeDefined();
    expect(apiRoute?.kind).toBe('api');
    expect(apiRoute?.methods).toContain('GET');
  });

  it('should match page routes with params', () => {
    const routes = router.getRoutes();
    const pageRoute = routes.find(r => r.path === '/users/:id');
    expect(pageRoute).toBeDefined();
    expect(pageRoute?.kind).toBe('page');
  });
});

Dependencias

  • @jamx-framework/core - Para NotFoundException y tipos base
  • @jamx-framework/server - Para JamxRequest, JamxResponse, RequestDispatcher
  • @jamx-framework/renderer - Para SSRRenderer en páginas
  • @types/node - Tipos de Node.js
  • vitest - Framework de testing
  • rimraf - Limpieza de directorios

Ejemplo completo

Proyecto completo

// jamx.config.ts
import { defineConfig } from '@jamx-framework/config';

export default defineConfig({
  targets: ['web'],
});
// src/api/health.handler.ts
export default {
  GET: async (req, res) => {
    res.json({ status: 'ok', timestamp: Date.now() });
  },
};
// src/api/users/[id].handler.ts
export default {
  GET: async (req, res) => {
    const id = req.params.id;
    const user = await db.users.findById(id);
    if (!user) {
      res.notFound('User not found');
      return;
    }
    res.json(user);
  },

  PUT: async (req, res) => {
    const id = req.params.id;
    const updates = req.body;
    const user = await db.users.update(id, updates);
    res.json(user);
  },

  DELETE: async (req, res) => {
    const id = req.params.id;
    await db.users.delete(id);
    res.noContent();
  },
};
// src/pages/index.page.tsx
import { jsx } from '@jamx-framework/renderer';

export default {
  render(ctx) {
    return jsx('div', { class: 'container' }, [
      jsx('h1', {}, 'Mi Aplicación JAMX'),
      jsx('p', {}, 'Bienvenido a la página de inicio'),
      jsx('a', { href: '/about' }, 'Acerca de'),
    ]);
  },

  meta(ctx) {
    return {
      title: 'Inicio',
      description: 'Página de inicio de mi aplicación',
    };
  },
};
// src/pages/about.page.tsx
import { jsx } from '@jamx-framework/renderer';

export default {
  render(ctx) {
    return jsx('div', { class: 'about' }, [
      jsx('h1', {}, 'Acerca de'),
      jsx('p', {}, 'Esta aplicación usa JAMX Framework'),
    ]);
  },

  meta(ctx) {
    return {
      title: 'Acerca de',
    };
  },
};
// server/index.ts
import { createServer } from '@jamx-framework/server';
import { FileRouter } from '@jamx-framework/router';

async function main() {
  const server = await createServer();

  const router = new FileRouter({ projectRoot: './' });
  server.use(router.dispatch.bind(router));

  await server.listen({ port: 3000 });
  console.log('Server running on http://localhost:3000');
}

main().catch(console.error);

Limitaciones

  • Solo archivos .handler.ts y .page.tsx: No soporta otras extensiones
  • Estructura fija: Requiere src/api/ y src/pages/
  • Sin middleware por ruta: Middleware debe ser global en el servidor
  • Carga dinámica: Los handlers se cargan con import() en cada request (cacheable)
  • Sin validación de params: Los params son strings, no hay validación automática

Buenas prácticas

1. Organización de archivos

src/
├── api/
│   ├── auth/
│   │   ├── login.handler.ts
│   │   └── logout.handler.ts
│   ├── users.handler.ts
│   └── users/
│       ├── [id].handler.ts
│       └── [id]/profile.handler.ts
└── pages/
    ├── index.page.tsx
    ├── about.page.tsx
    ├── users/
    │   ├── index.page.tsx
    │   └── [id].page.tsx
    └── admin/
        └── dashboard.page.tsx

2. Separación de lógica

// src/api/users.handler.ts
import { getUserService } from '../services/user-service.js';

export default {
  GET: async (req, res) => {
    const service = getUserService();
    const users = await service.list();
    res.json(users);
  },
};

3. Manejo de errores

// src/api/users/[id].handler.ts
export default {
  GET: async (req, res) => {
    try {
      const user = await db.users.findById(req.params.id);
      if (!user) {
        res.notFound('User not found');
        return;
      }
      res.json(user);
    } catch (err) {
      console.error('Error fetching user:', err);
      res.json({ error: 'Internal Server Error' }, 500);
    },
  },
};

4. Usar tipos para params

// src/api/users/[id].handler.ts
interface UserParams {
  id: string;
}

export default {
  GET: async (req, res) => {
    const params = req.params as UserParams;
    const user = await db.users.findById(params.id);
    res.json(user);
  },
};

Integración con compilador

El router se integra con @jamx-framework/compiler:

  1. El compilador llama a router.getRoutes() para obtener todas las rutas
  2. Genera un archivo AppRoutes con tipos fuertes
  3. Permite navegación type-safe en el cliente
// .generated/routes.ts (generado automáticamente)
export const AppRoutes = {
  '/api/users': { method: 'GET' | 'POST' },
  '/api/users/:id': { method: 'GET' | 'PUT' | 'DELETE' },
  '/': { page: true },
  '/about': { page: true },
  '/users/:id': { page: true },
};

Preguntas frecuentes

¿Cómo manejar middleware global?

server.use(async (req, res, next) => {
  // Logging
  console.log(`${req.method} ${req.path}`);

  // Auth
  const token = req.headers.authorization?.split(' ')[1];
  if (token) {
    req.locals.user = await verifyToken(token);
  }

  next();
});

¿Cómo manejar 404?

El router lanza NotFoundException automáticamente. El servidor debe tener un error handler:

server.use(async (err, req, res) => {
  if (err.code === 'ROUTE_NOT_FOUND') {
    res.json({ error: 'Not Found' }, 404);
    return;
  }
  next(err);
});

¿Puedo tener rutas estáticas (sin handler)?

Sí, usa server.use(staticFiles) antes del router:

import { staticFiles } from '@jamx-framework/server';

server.use(staticFiles({ root: './public' }));
server.use(router.dispatch.bind(router));

¿Cómo deshabilitar HMR?

const router = new FileRouter({ projectRoot: './' });
// No llamar a invalidate() automáticamente

¿Puedo pre-cargar handlers?

Sí, llama a initialize() al startup:

const router = new FileRouter({ projectRoot: './' });
await router.initialize(); // Pre-cargar todas las rutas

Rendimiento

  • Cache de módulos: Node.js cachea import() automáticamente
  • Lazy loading: Los handlers se cargan solo cuando se solicitan
  • Inicialización una vez: initialize() se llama automáticamente solo la primera vez
  • Matching O(n): Busca en orden de registro; para muchas rutas considera un trie

Comparación con otros routers

| Característica | JAMX Router | Express | Next.js | |----------------|-------------|---------|---------| | Enrutamiento | File-based | Code-based | File-based | | SSR | Sí (con renderer) | No | Sí | | API routes | Sí | Sí | Sí | | Parámetros dinámicos | Sí (:id) | Sí (:id) | Sí ([id]) | | Wildcards | Sí (*) | Sí (*) | No | | Middleware | Global | Global + por ruta | Middleware global | | Hot Reload | Sí (invalidate) | No | Sí |

Referencia rápida de patrones

| Patrón de archivo | Ruta resultante | Tipo | |-------------------|-----------------|------| | api/users.handler.ts | /api/users | API | | api/users/[id].handler.ts | /api/users/:id | API | | api/posts/[postId]/comments/[id].handler.ts | /api/posts/:postId/comments/:id | API | | pages/index.page.tsx | / | Página | | pages/about.page.tsx | /about | Página | | pages/users/[id].page.tsx | /users/:id | Página | | pages/blog/[year]/[month]/[slug].page.tsx | /blog/:year/:month/:slug | Página | | pages/admin/*.page.tsx | /admin/* | Página (wildcard) |

Archivos importantes

  • src/router.ts - FileRouter principal
  • src/scanner/file-scanner.ts - Escaneo de sistema de archivos
  • src/scanner/route-parser.ts - Conversión de paths a rutas
  • src/matcher/matcher.ts - Algoritmo de matching
  • src/handler/api-handler.ts - Ejecutor de API handlers
  • src/handler/page-handler.ts - Ejecutor de páginas
  • tests/unit/matcher/matcher.test.ts - Tests de matching
  • tests/unit/scanner/route-parser.test.ts - Tests de parseo