Skip to content

criszst/RouteX

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

117 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

RouteX

Bun TypeScript License

A lightweight, Express-inspired HTTP framework focused on clarity, performance, and minimalism β€” built from scratch in TypeScript/Bun.


Table of Contents


Checklist

  • Basic app structure (send, post, res, req, next)
  • Reduced dependencies β€” core libs rebuilt from scratch
  • Strong TypeScript interfaces
  • Static file support (sendFile)
  • Route aliases for cleaner code
  • IP middleware for blocking & rate-limiting
  • Hot Module Reload for dev productivity
  • Custom 404 handler
  • Build-time route compilation
  • Trie-based runtime route matcher
  • Query string parsing
  • Dynamic route params (:id)
  • Middleware pipeline with next()
  • Logger middleware
  • Expanded test coverage

Architecture

RouteX 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.

Core concepts

1. Layer β€” the unit of registration

Every app.get(), app.post(), or app.use() call creates a Layer and pushes it onto the router's internal stack:

{
  path:    '/users/:id',
  aliases: ['/users/:id', '/u/:id'],  // alternative paths
  methods: Set { 'GET' },
  handler: Function,
  type:    'route' | 'middleware'
}

2. The Stack β€” ordered list of Layers

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.

router.stack
β”œβ”€β”€ Layer { path: '/',        type: 'middleware', methods: ANY  }  ← init middleware
β”œβ”€β”€ Layer { path: '/users',   type: 'route',      methods: GET  }
β”œβ”€β”€ Layer { path: '/users/:id', type: 'route',    methods: GET  }
└── Layer { path: '/posts',   type: 'route',      methods: POST }

3. The Trie Compiler β€” Stack β†’ CompiledNode tree

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:

Registered routes:
  GET  /users
  GET  /users/:id
  POST /users/:id

Compiled trie:
  root
  β”‚  [ANY] β†’ initMiddleware
  β”‚
  └── "users"
        [GET] β†’ getUsersHandler
        └── :id  (paramName = "id")
              [GET]  β†’ getUserByIdHandler
              [POST] β†’ updateUserHandler

Dynamic segments (:param) become a paramChild node with a paramName property. Static segments become entries in the children Map.

4. The Matcher β€” O(k) trie traversal

RouterMatcher.match(method, url) traverses the compiled trie segment by segment:

  1. Strip the query string from the URL
  2. Split the path into segments (/users/42 β†’ ['users', '42'])
  3. Walk the trie β€” static match first, then paramChild fallback
  4. On a match, collect params (e.g. { id: '42' }) and query (parsed query string)
  5. Look up the HTTP method handler on the final node

No file loading, no regex scanning, no linear search β€” just map lookups down a tree.

5. Middleware pipeline β€” chained next()

Handlers 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:

// Conceptually, per matched node:
let index = 0;
const next = () => {
  const handler = handlers[index++];
  if (!handler) return;
  handler(req, res, next);
};
next();

PipelineCompiler.compilePipeline() provides the async version of this pattern.

6. Hot Reload β€” dev only

In development mode, RouteManager uses chokidar to watch the routes directory. On any file change:

  1. All route-type layers are removed from router.stack (middleware layers are preserved)
  2. The changed route file is re-required (module cache is cleared first)
  3. router.rebuild() recompiles the trie from the updated stack
[chokidar detects change]
        ↓
router.stack.filter(l => l.type !== 'route')   ← clear old routes
        ↓
delete require.cache[...] + require(file)       ← reload file
        ↓
router.rebuild()                                ← recompile trie

Hot reload does not run in production.

Full request lifecycle

STARTUP
β”œβ”€β”€ RouteManager.loadRoutes()
β”‚   └── require() each route file β†’ app.get/post/use() β†’ stack.push(Layer)
β”œβ”€β”€ app.listen()
β”‚   └── router.compile()
β”‚       └── RouterCompiler.compile(stack) β†’ builds CompiledNode trie
└── http.createServer(app).listen(port)

─────────────────────────────────────────────

RUNTIME  (per request)
β”œβ”€β”€ app(req, res)
β”‚   └── prototype.handle()
β”‚       β”œβ”€β”€ Response.send/json/redirect/... attached to res
β”‚       └── router.handle(req, res)
β”‚           β”œβ”€β”€ RouterMatcher.match(method, url)
β”‚           β”‚   β”œβ”€β”€ Parse query string
β”‚           β”‚   β”œβ”€β”€ Split path into segments
β”‚           β”‚   β”œβ”€β”€ Walk trie (static β†’ param fallback)
β”‚           β”‚   └── Return { handler[], params, query } or null
β”‚           β”‚
β”‚           β”œβ”€β”€ null β†’ 404 response
β”‚           └── match β†’ req.params = params
β”‚                        req.query  = query
β”‚                        next() pipeline β†’ handler(req, res, next)

Project Structure

src/
β”œβ”€β”€ api/
β”‚   β”œβ”€β”€ index.ts              # Entry point β€” loads routes, starts server
β”‚   └── routex.ts             # createApp() factory β€” merges prototype onto app
β”‚
β”œβ”€β”€ core/
β”‚   β”œβ”€β”€ layer/
β”‚   β”‚   └── layer.ts          # Layer class β€” single route/middleware unit
β”‚   β”œβ”€β”€ router/
β”‚   β”‚   β”œβ”€β”€ CompiledNode.ts   # Trie node (children map, paramChild, handlers map)
β”‚   β”‚   β”œβ”€β”€ RouterCompiler.ts # Compiles Layer stack β†’ CompiledNode trie
β”‚   β”‚   β”œβ”€β”€ RouterMatcher.ts  # Traverses trie, parses params & query
β”‚   β”‚   β”œβ”€β”€ PipelineCompiler.ts # Builds async middleware chain
β”‚   β”‚   └── router.ts         # Router class β€” stack, compile(), rebuild(), handle()
β”‚   └── types/
β”‚       β”œβ”€β”€ IApp.ts           # App interface
β”‚       β”œβ”€β”€ IRouteHandler.ts  # Handler function type
β”‚       β”œβ”€β”€ IOptionsFile.ts   # sendFile options
β”‚       β”œβ”€β”€ IDetails.ts       # Error detail shape
β”‚       └── IProtoype.ts      # GetOptions type
β”‚
β”œβ”€β”€ http/
β”‚   β”œβ”€β”€ errors/
β”‚   β”‚   └── details.ts        # Structured error factory
β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   β”œβ”€β”€ init.ts           # Sets res prototype on each request
β”‚   β”‚   β”œβ”€β”€ ip.ts             # IP blocking / rate-limit middleware
β”‚   β”‚   └── prototype.ts      # App prototype β€” handle, get, post, use, listen, lazyrouter
β”‚   β”œβ”€β”€ request/
β”‚   β”‚   β”œβ”€β”€ IServerRequest.ts # Extends IncomingMessage with params, query, body...
β”‚   β”‚   └── request.ts        # Request helper class
β”‚   └── response/
β”‚       β”œβ”€β”€ IServerResponse.ts # Extends ServerResponse with send, json, redirect...
β”‚       └── response.ts        # Static methods that attach response helpers to res
β”‚
β”œβ”€β”€ middleware/
β”‚   β”œβ”€β”€ logger/
β”‚   β”‚   └── LoggerMiddleware.ts
β”‚   β”œβ”€β”€ RouteManager.ts       # Route file discovery, loading, hot reload
β”‚   └── RouteMiddleware.ts    # RouteXMiddleware type (req, res, next)
β”‚
β”œβ”€β”€ utils/
β”‚   β”œβ”€β”€ flatten.ts            # Array flattening helper
β”‚   └── merge.ts              # Object property merging (used in createApp)
β”‚
β”œβ”€β”€ examples/
β”‚   └── routes/               # Example route files (one export per file)
β”‚       β”œβ”€β”€ main.ts
β”‚       β”œβ”€β”€ params.ts
β”‚       β”œβ”€β”€ json.ts
β”‚       β”œβ”€β”€ redirect.ts
β”‚       └── send.ts
β”‚
β”œβ”€β”€ __mocks__/
β”‚   β”œβ”€β”€ mime.mock.ts
β”‚   └── response.mock.ts
β”‚
└── tests/
    β”œβ”€β”€ app.test.ts
    β”œβ”€β”€ prototype.test.ts
    └── response.test.ts

Features

Method Description
res.send(data) Sends plain text or serialized object
res.json(data) Sends a JSON response with correct headers
res.download(path) Forces a file download
res.redirect(url) 302 redirect
res.sendFile(path, options?) Serves a static file with optional headers and cache control
req.params Dynamic route parameters (/users/:id β†’ { id: '42' })
req.query Parsed query string (?a=1&b=2 β†’ { a: '1', b: '2' })
Route aliases Register the same handler under multiple paths

Usage

Registering routes

import { app } from './api/routex';

// Simple route
app.get('/users', { aliases: '/u' }, (req, res) => {
  res.json({ users: [] });
});

// Dynamic param
app.get('/users/:id', {}, (req, res) => {
  res.json({ id: req.params.id });
});

// POST route
app.post('/users', {}, (req, res) => {
  res.json({ created: true });
});

// Custom 404
app.setCustom404((req, res) => {
  res.statusCode = 404;
  res.json({ error: 'Not found' });
});

Response methods

res.send("Hello, world!");
res.json({ hello: "world" });
res.redirect("https://example.com");
res.download("./report.pdf");
res.sendFile("./index.html", { root: process.cwd() });

Route files (convention)

Each file in src/examples/routes/ exports a default function. RouteManager discovers and loads them automatically:

// src/examples/routes/users.ts
import { app } from '../../api/routex';

export default function usersRoutes() {
  app.get('/users', {}, (req, res) => {
    res.json({ users: [] });
  });
}

Getting Started

git clone https://github.com/criszst/RouteX.git
cd RouteX
bun install

# Development (hot reload enabled)
bun dev

# Production build + start
bun start

# Tests
bun test

The server runs on http://localhost:3000 by default.

curl http://localhost:3000/
# {"hello":"world"}

curl http://localhost:3000/json
# {"json":"test for json method"}

About

πŸš€ | A lightweight, Express-inspired framework focused on clarity, performance, and minimalism.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors