@jamx-framework/router
v1.0.1
Published
JAMX Framework — File-based Router
Downloads
115
Maintainers
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:
- Escaneo:
FileScannerrecorre el proyecto buscando archivos*.handler.ts(API) y*.page.tsx(páginas) - Parseo:
RouteParserconvierte nombres de archivos y carpetas en definiciones de rutas con parámetros - Matching:
RouteMatcherempareja URLs entrantes con rutas registradas, extrayendo parámetros - Ejecución:
ApiHandlerExecutoroPageHandlerExecutorejecutan el handler correspondiente
Componentes principales
FileRouter (src/router.ts)
Clase principal que implementa RequestDispatcher:
initialize(): Escanea el proyecto y construye la tabla de rutasdispatch(req, res): Despacha una request al handler correctogetRoutes(): 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.tsensrc/api/ - Busca archivos
*.page.tsxensrc/pages/ - Construye objetos
Routecon 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
reqyresde JAMX
PageHandlerExecutor (src/handler/page-handler.ts)
Ejecuta componentes de página:
- Carga el módulo dinámicamente
- Usa
SSRRendererpara 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/loginEstructura 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/:idEstructura 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.jsonDefinir 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:
- Busca
src/api/**/*.handler.ts→ rutas API - Busca
src/pages/**/*.page.tsx→ rutas de página - Convierte paths a patrones de ruta
- 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): RouteConvierte un path de archivo en una definición de ruta.
Reglas:
api/→ prefijo/apipages/→ 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/dashboardRouteMatcher
setRoutes()
setRoutes(routes: Route[]): voidCarga las rutas para matching.
match()
match(path: string, method: HttpMethod): RouteMatch | nullBusca una ruta que coincida con el path y método.
Algoritmo:
- Iterar rutas en orden de registro
- Para cada ruta, comparar segmento por segmento
- Si encuentra
:param, extraer valor - Si encuentra
*, hacer match con cualquier resto - Si todos los segmentos coinciden, retornar
RouteMatch - 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:
- Cargar módulo desde
route.filePath - Obtener handler por método HTTP (
route.methods+default) - Si no existe handler para el método → 405
- Invocar handler con
reqyres - 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:
- Cargar módulo desde
route.filePath - Obtener componente
default export - Crear
RenderContextdesdereq - Invocar
SSRRenderer.render(component, ctx) - Enviar HTML en
res.bodycon 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 JavaScriptpnpm dev- Compilación en watch modepnpm test- Ejecuta tests unitariospnpm test:watch- Tests en watch modepnpm type-check- Verifica tipos sin compilarpnpm 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- ParaNotFoundExceptiony tipos base@jamx-framework/server- ParaJamxRequest,JamxResponse,RequestDispatcher@jamx-framework/renderer- ParaSSRRendereren páginas@types/node- Tipos de Node.jsvitest- Framework de testingrimraf- 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.tsy.page.tsx: No soporta otras extensiones - Estructura fija: Requiere
src/api/ysrc/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.tsx2. 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:
- El compilador llama a
router.getRoutes()para obtener todas las rutas - Genera un archivo
AppRoutescon tipos fuertes - 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 rutasRendimiento
- 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 principalsrc/scanner/file-scanner.ts- Escaneo de sistema de archivossrc/scanner/route-parser.ts- Conversión de paths a rutassrc/matcher/matcher.ts- Algoritmo de matchingsrc/handler/api-handler.ts- Ejecutor de API handlerssrc/handler/page-handler.ts- Ejecutor de páginastests/unit/matcher/matcher.test.ts- Tests de matchingtests/unit/scanner/route-parser.test.ts- Tests de parseo
