{"id":28376235,"url":"https://github.com/criszst/routex","last_synced_at":"2026-05-05T11:32:07.330Z","repository":{"id":274568874,"uuid":"923330693","full_name":"criszst/RouteX","owner":"criszst","description":"🚀 | Ligthweight Web Routing Engine inspired by Express","archived":false,"fork":false,"pushed_at":"2025-06-29T02:25:00.000Z","size":4814,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-29T03:40:27.905Z","etag":null,"topics":["express","express-ts","express-typescript","jest","jest-tests","ts","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/criszst.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-01-28T03:19:45.000Z","updated_at":"2025-06-29T02:25:03.000Z","dependencies_parsed_at":"2025-03-02T00:20:37.792Z","dependency_job_id":"c46d4942-88cd-4cb2-846b-c39568a08544","html_url":"https://github.com/criszst/RouteX","commit_stats":null,"previous_names":["criszst/myexpress","criszst/routex"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/criszst/RouteX","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criszst%2FRouteX","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criszst%2FRouteX/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criszst%2FRouteX/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criszst%2FRouteX/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/criszst","download_url":"https://codeload.github.com/criszst/RouteX/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/criszst%2FRouteX/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270099697,"owners_count":24527037,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-12T02:00:09.011Z","response_time":80,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["express","express-ts","express-typescript","jest","jest-tests","ts","typescript"],"created_at":"2025-05-30T00:06:01.266Z","updated_at":"2026-05-05T11:32:07.321Z","avatar_url":"https://github.com/criszst.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RouteX\n\n![Bun](https://img.shields.io/badge/bun-%3E=1.3.3-green)\n![TypeScript](https://img.shields.io/badge/types-checked-blue)\n![License](https://img.shields.io/github/license/criszst/RouteX)\n\nA lightweight, Express-inspired HTTP framework focused on **clarity, performance, and minimalism** — built from scratch in TypeScript/Bun.\n\n---\n\n## Table of Contents\n\n* [Checklist](#checklist)\n* [Architecture](#architecture)\n* [Project Structure](#project-structure)\n* [Features](#features)\n* [Usage](#usage)\n* [Getting Started](#getting-started)\n\n---\n\n## Checklist\n\n* [x] Basic app structure (`send`, `post`, `res`, `req`, `next`)\n* [x] Reduced dependencies — core libs rebuilt from scratch\n* [x] Strong TypeScript interfaces\n* [x] Static file support (`sendFile`)\n* [x] Route aliases for cleaner code\n* [x] IP middleware for blocking \u0026 rate-limiting\n* [x] Hot Module Reload for dev productivity\n* [x] Custom 404 handler\n* [x] Build-time route compilation\n* [x] Trie-based runtime route matcher\n* [x] Query string parsing\n* [x] Dynamic route params (`:id`)\n* [x] Middleware pipeline with `next()`\n* [ ] Logger middleware\n* [ ] Expanded test coverage\n\n---\n\n## Architecture\n\nRouteX follows a **build-time route compilation** approach — routes are compiled into a data structure once at startup, and every incoming request is matched against that structure in O(k) time, where `k` is the number of path segments.\n\n### Core concepts\n\n#### 1. Layer — the unit of registration\n\nEvery `app.get()`, `app.post()`, or `app.use()` call creates a `Layer` and pushes it onto the router's internal **stack**:\n\n```ts\n{\n  path:    '/users/:id',\n  aliases: ['/users/:id', '/u/:id'],  // alternative paths\n  methods: Set { 'GET' },\n  handler: Function,\n  type:    'route' | 'middleware'\n}\n```\n\n#### 2. The Stack — ordered list of Layers\n\n`router.stack` is an ordered array of `Layer` objects, accumulated at startup. The stack is never consulted at request time — it exists solely as input to the compiler.\n\n```\nrouter.stack\n├── Layer { path: '/',        type: 'middleware', methods: ANY  }  ← init middleware\n├── Layer { path: '/users',   type: 'route',      methods: GET  }\n├── Layer { path: '/users/:id', type: 'route',    methods: GET  }\n└── Layer { path: '/posts',   type: 'route',      methods: POST }\n```\n\n#### 3. The Trie Compiler — Stack → CompiledNode tree\n\n`RouterCompiler.compile(stack)` walks every layer and builds a **prefix trie** (also called a radix-style route tree). Each path segment becomes a trie node:\n\n```\nRegistered routes:\n  GET  /users\n  GET  /users/:id\n  POST /users/:id\n\nCompiled trie:\n  root\n  │  [ANY] → initMiddleware\n  │\n  └── \"users\"\n        [GET] → getUsersHandler\n        └── :id  (paramName = \"id\")\n              [GET]  → getUserByIdHandler\n              [POST] → updateUserHandler\n```\n\nDynamic segments (`:param`) become a `paramChild` node with a `paramName` property. Static segments become entries in the `children` Map.\n\n#### 4. The Matcher — O(k) trie traversal\n\n`RouterMatcher.match(method, url)` traverses the compiled trie segment by segment:\n\n1. Strip the query string from the URL\n2. Split the path into segments (`/users/42` → `['users', '42']`)\n3. Walk the trie — static match first, then `paramChild` fallback\n4. On a match, collect `params` (e.g. `{ id: '42' }`) and `query` (parsed query string)\n5. Look up the HTTP method handler on the final node\n\nNo file loading, no regex scanning, no linear search — just map lookups down a tree.\n\n#### 5. Middleware pipeline — chained `next()`\n\nHandlers stored on each trie node are executed as a **pipeline**. Each handler receives `(req, res, next)` and calls `next()` to pass control to the next handler in the chain:\n\n```ts\n// Conceptually, per matched node:\nlet index = 0;\nconst next = () =\u003e {\n  const handler = handlers[index++];\n  if (!handler) return;\n  handler(req, res, next);\n};\nnext();\n```\n\n`PipelineCompiler.compilePipeline()` provides the async version of this pattern.\n\n#### 6. Hot Reload — dev only\n\nIn development mode, `RouteManager` uses **chokidar** to watch the routes directory. On any file change:\n\n1. All `route`-type layers are removed from `router.stack` (middleware layers are preserved)\n2. The changed route file is re-required (module cache is cleared first)\n3. `router.rebuild()` recompiles the trie from the updated stack\n\n```\n[chokidar detects change]\n        ↓\nrouter.stack.filter(l =\u003e l.type !== 'route')   ← clear old routes\n        ↓\ndelete require.cache[...] + require(file)       ← reload file\n        ↓\nrouter.rebuild()                                ← recompile trie\n```\n\nHot reload does **not** run in production.\n\n### Full request lifecycle\n\n```\nSTARTUP\n├── RouteManager.loadRoutes()\n│   └── require() each route file → app.get/post/use() → stack.push(Layer)\n├── app.listen()\n│   └── router.compile()\n│       └── RouterCompiler.compile(stack) → builds CompiledNode trie\n└── http.createServer(app).listen(port)\n\n─────────────────────────────────────────────\n\nRUNTIME  (per request)\n├── app(req, res)\n│   └── prototype.handle()\n│       ├── Response.send/json/redirect/... attached to res\n│       └── router.handle(req, res)\n│           ├── RouterMatcher.match(method, url)\n│           │   ├── Parse query string\n│           │   ├── Split path into segments\n│           │   ├── Walk trie (static → param fallback)\n│           │   └── Return { handler[], params, query } or null\n│           │\n│           ├── null → 404 response\n│           └── match → req.params = params\n│                        req.query  = query\n│                        next() pipeline → handler(req, res, next)\n```\n\n---\n\n## Project Structure\n\n```\nsrc/\n├── api/\n│   ├── index.ts              # Entry point — loads routes, starts server\n│   └── routex.ts             # createApp() factory — merges prototype onto app\n│\n├── core/\n│   ├── layer/\n│   │   └── layer.ts          # Layer class — single route/middleware unit\n│   ├── router/\n│   │   ├── CompiledNode.ts   # Trie node (children map, paramChild, handlers map)\n│   │   ├── RouterCompiler.ts # Compiles Layer stack → CompiledNode trie\n│   │   ├── RouterMatcher.ts  # Traverses trie, parses params \u0026 query\n│   │   ├── PipelineCompiler.ts # Builds async middleware chain\n│   │   └── router.ts         # Router class — stack, compile(), rebuild(), handle()\n│   └── types/\n│       ├── IApp.ts           # App interface\n│       ├── IRouteHandler.ts  # Handler function type\n│       ├── IOptionsFile.ts   # sendFile options\n│       ├── IDetails.ts       # Error detail shape\n│       └── IProtoype.ts      # GetOptions type\n│\n├── http/\n│   ├── errors/\n│   │   └── details.ts        # Structured error factory\n│   ├── middleware/\n│   │   ├── init.ts           # Sets res prototype on each request\n│   │   ├── ip.ts             # IP blocking / rate-limit middleware\n│   │   └── prototype.ts      # App prototype — handle, get, post, use, listen, lazyrouter\n│   ├── request/\n│   │   ├── IServerRequest.ts # Extends IncomingMessage with params, query, body...\n│   │   └── request.ts        # Request helper class\n│   └── response/\n│       ├── IServerResponse.ts # Extends ServerResponse with send, json, redirect...\n│       └── response.ts        # Static methods that attach response helpers to res\n│\n├── middleware/\n│   ├── logger/\n│   │   └── LoggerMiddleware.ts\n│   ├── RouteManager.ts       # Route file discovery, loading, hot reload\n│   └── RouteMiddleware.ts    # RouteXMiddleware type (req, res, next)\n│\n├── utils/\n│   ├── flatten.ts            # Array flattening helper\n│   └── merge.ts              # Object property merging (used in createApp)\n│\n├── examples/\n│   └── routes/               # Example route files (one export per file)\n│       ├── main.ts\n│       ├── params.ts\n│       ├── json.ts\n│       ├── redirect.ts\n│       └── send.ts\n│\n├── __mocks__/\n│   ├── mime.mock.ts\n│   └── response.mock.ts\n│\n└── tests/\n    ├── app.test.ts\n    ├── prototype.test.ts\n    └── response.test.ts\n```\n\n---\n\n## Features\n\n| Method | Description |\n|---|---|\n| `res.send(data)` | Sends plain text or serialized object |\n| `res.json(data)` | Sends a JSON response with correct headers |\n| `res.download(path)` | Forces a file download |\n| `res.redirect(url)` | 302 redirect |\n| `res.sendFile(path, options?)` | Serves a static file with optional headers and cache control |\n| `req.params` | Dynamic route parameters (`/users/:id` → `{ id: '42' }`) |\n| `req.query` | Parsed query string (`?a=1\u0026b=2` → `{ a: '1', b: '2' }`) |\n| Route aliases | Register the same handler under multiple paths |\n\n---\n\n## Usage\n\n### Registering routes\n\n```ts\nimport { app } from './api/routex';\n\n// Simple route\napp.get('/users', { aliases: '/u' }, (req, res) =\u003e {\n  res.json({ users: [] });\n});\n\n// Dynamic param\napp.get('/users/:id', {}, (req, res) =\u003e {\n  res.json({ id: req.params.id });\n});\n\n// POST route\napp.post('/users', {}, (req, res) =\u003e {\n  res.json({ created: true });\n});\n\n// Custom 404\napp.setCustom404((req, res) =\u003e {\n  res.statusCode = 404;\n  res.json({ error: 'Not found' });\n});\n```\n\n### Response methods\n\n```ts\nres.send(\"Hello, world!\");\nres.json({ hello: \"world\" });\nres.redirect(\"https://example.com\");\nres.download(\"./report.pdf\");\nres.sendFile(\"./index.html\", { root: process.cwd() });\n```\n\n### Route files (convention)\n\nEach file in `src/examples/routes/` exports a default function. RouteManager discovers and loads them automatically:\n\n```ts\n// src/examples/routes/users.ts\nimport { app } from '../../api/routex';\n\nexport default function usersRoutes() {\n  app.get('/users', {}, (req, res) =\u003e {\n    res.json({ users: [] });\n  });\n}\n```\n\n---\n\n## Getting Started\n\n```bash\ngit clone https://github.com/criszst/RouteX.git\ncd RouteX\nbun install\n\n# Development (hot reload enabled)\nbun dev\n\n# Production build + start\nbun start\n\n# Tests\nbun test\n```\n\nThe server runs on **[http://localhost:3000](http://localhost:3000)** by default.\n\n```bash\ncurl http://localhost:3000/\n# {\"hello\":\"world\"}\n\ncurl http://localhost:3000/json\n# {\"json\":\"test for json method\"}\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcriszst%2Froutex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcriszst%2Froutex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcriszst%2Froutex/lists"}