Skip to content

Conversation

marshallswain
Copy link
Member

@marshallswain marshallswain commented Sep 1, 2025

Router Architecture Overhaul & Performance Optimization

🚀 Performance Improvements

  • 48% performance improvement in route lookup operations through
    optimized algorithms and caching
  • Enhanced object lookups and streamlined matching logic for better V8
    performance

🏗️ New Modular Architecture

  • Pluggable router system with RouterInterface - swap router
    implementations at runtime
  • New @feathersjs/routing package with Express and Koa-compatible routers
  • Clean separation between core routing logic and framework-specific
    implementations

✨ Enhanced Routing Features

  • Catch-all routes with ::param syntax for handling wildcards (e.g.,
    /docs/::path)
  • Improved route matching with better placeholder and parameter handling
  • Full backward compatibility with existing v5 routing patterns

🔧 Code Quality

  • DRY refactoring eliminates code duplication between Express/Koa
    implementations
  • Comprehensive test coverage for all new features and performance
    optimizations
  • Type-safe router swapping with full TypeScript support

This enables both better performance for existing apps and flexible routing
architectures for complex use cases.

- Add Object.create(null) for children and pathCache objects
- Replace startsWith(':') with direct array access current[0] === ':'
- Implement manual loops instead of array methods for placeholders
- Add path caching with fast paths for common cases
- Use in-place object mutation instead of object spreading
- Add comprehensive performance benchmark showing 48-49% improvement
- Preserve old implementation as router2.ts for comparison
- Update tests to expect optimized Object.create(null) params
@marshallswain marshallswain marked this pull request as draft September 1, 2025 14:56
Adds support for catch-all routes using ::param syntax that capture remaining path segments as arrays.

Key features:
- ::param syntax captures all remaining segments as string[]
- Proper priority: exact → named params → catch-all
- Validates catch-all params only at end of routes
- Maintains 48% performance improvement over old router
- Full TypeScript support with string | string[] param types

Examples:
- /docs/::path matches /docs/api/guide → params.path = ['api', 'guide']
- /api/:version/docs/::path combines named and catch-all params
- /docs exact route takes priority over /docs/::path catch-all

This completes router feature parity for full Express/Koa independence.
- Add RouterInterface abstraction allowing custom router implementations
- Update Feathers application to support router replacement via app.routes assignment
- Add comprehensive tests for router swapping with mock implementations
- Remove obsolete router2.ts and performance benchmarks
- Clean up router tests with proper TypeScript typing
- Maintain RouterInterface compatibility for external router implementations
- Support router.caseSensitive property for case-insensitive routing
- Create new @feathersjs/routing package for Express/Koa migration support
- Implement ExpressRouter using path-to-regexp for 100% Express compatibility
- Implement KoaRouter using path-to-regexp for 100% Koa compatibility
- Support named parameters (:id), wildcards (*), optional parameters, and regex constraints
- Add comprehensive tests for both routers including Feathers integration
- Include TypeScript definitions for path-to-regexp
- Provide clean API: app.routes = new ExpressRouter() or app.routes = new KoaRouter()
- Runtime agnostic - works across Node.js, Deno, Bun, and Cloudflare Workers
Copy link

cloudflare-workers-and-pages bot commented Sep 2, 2025

Deploying feathers-dove with  Cloudflare Pages  Cloudflare Pages

Latest commit: f5f4d55
Status: ✅  Deploy successful!
Preview URL: https://3fe91a60.feathers.pages.dev
Branch Preview URL: https://v6-router-optimization.feathers.pages.dev

View logs

- Extract shared logic into BaseRouter base class
- Maintain framework-specific defaults (Express: case insensitive, Koa: case sensitive)
- Fix path-to-regexp v8 compatibility with named wildcard syntax (/*path)
- Remove test expectations for __id parameter (router-specific behavior)
- Reduce code duplication by 68% while preserving all functionality
- All 34 routing tests pass with full Feathers service integration

Breaking changes:
- Wildcard routes now use named syntax: /docs/*path instead of /docs/*
- Parameter extraction returns 'path' key instead of '*' for wildcards
- Add BaseRouter tests to cover constructor options and edge cases
- Change router tests to import from index.ts (realistic usage pattern)
- Remove redundant index.test.ts file
- Achieve 100% statement coverage and 92.59% branch coverage
- Remaining uncovered branches are defensive fallbacks for unreachable edge cases

Coverage improvements:
- index.ts: 0% → 100% coverage through realistic imports
- base-router.ts: better branch coverage for constructor options
- All tests now use the same import pattern as end users
placeholders: RouteNode[] = []
catchAll?: RouteNode<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we need a catchAll?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one ironically came up twice in the last couple days. While I was making an npm server with Feathers over the weekend, I found a route that legitimately needed to capture a dynamic number of params. I was able to work around it, but the dynamic params would have made it cleaner. Then today Dallin had another use case that made sense to me. A root-level route that handles whatever other paths you throw at it:

/actions
/actions/test
/actions/test/test1/tes2

And with the catchAll feature, you can put a catch all custom param at the end of the route, so you could do this:

app.use('actions/::args', ..., { routeParams: ['args'] })

Then you get an array of args to handle as you please.

It's the most likely edge case to handle, so we cover 95% of routing use cases without any sacrifice of speed.

Then I realized that we had an opportunity to improve the upgrade path because there are likely users who are stuck with their old Express and Koa routes who might like to upgrade. You already kept the router decoupled, so I made an express router and koa router in a routing package. That might help people upgrade, or motivate them to do so if they feel like the upgrade path is easier.

// Fast path for root
if (!path || path === '/') {
const result = ['']
this.pathCache[path] = result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathCache is never cleaned up so anybody can crash the server by making a lot of unique URL requests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I added a simple LRU mechanism that defaults to 1000 cached paths before booting old ones.

@marshallswain marshallswain changed the title feat: optimize router performance with 49% improvement (2x) feat: pluggable router architecture with 49% performance improvement Sep 2, 2025
@marshallswain marshallswain marked this pull request as ready for review September 2, 2025 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants