JSPM

@jamx-framework/router

1.0.1
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 9
  • Score
    100M100P100Q70977F
  • License MIT

JAMX Framework — File-based Router

Package Exports

  • @jamx-framework/router

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
API routes
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

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