{"id":26476831,"url":"https://github.com/psenger/express-auto-router","last_synced_at":"2025-03-20T00:07:58.646Z","repository":{"id":282329154,"uuid":"948183473","full_name":"psenger/express-auto-router","owner":"psenger","description":"A dynamic route composition system for Express.js applications that automatically discovers and mount routes and middleware based on your file system structure. Inspired by Next.js routing conventions.","archived":false,"fork":false,"pushed_at":"2025-03-14T02:08:16.000Z","size":2,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-14T02:11:13.221Z","etag":null,"topics":["autorouting","discovery","expressjs","filebasedrouting","middleware","nodejs","restframework","routing"],"latest_commit_sha":null,"homepage":"https://psenger.github.io/express-auto-router/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/psenger.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","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}},"created_at":"2025-03-13T22:21:08.000Z","updated_at":"2025-03-14T02:08:19.000Z","dependencies_parsed_at":"2025-03-14T02:11:23.357Z","dependency_job_id":"88ffffbc-ebf4-4f32-aeb0-1ac60cc9401a","html_url":"https://github.com/psenger/express-auto-router","commit_stats":null,"previous_names":["psenger/express-auto-router"],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fexpress-auto-router","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fexpress-auto-router/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fexpress-auto-router/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fexpress-auto-router/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/psenger","download_url":"https://codeload.github.com/psenger/express-auto-router/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244525610,"owners_count":20466548,"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","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":["autorouting","discovery","expressjs","filebasedrouting","middleware","nodejs","restframework","routing"],"created_at":"2025-03-20T00:07:58.184Z","updated_at":"2025-03-20T00:07:58.636Z","avatar_url":"https://github.com/psenger.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [express-auto-router](https://github.com/psenger/express-auto-router#readme)\n\n\u003e [!TAG]\n\u003e 0.0.3\n\nA dynamic route composition system for Express.js applications that automatically discovers and mount routes and middleware based on your file system structure. Inspired by Next.js routing conventions.\n\n## Table of Contents\n\n\u003c!-- toc --\u003e\n\n- [Features](#features)\n- [Installation](#installation)\n- [How it Works](#how-it-works)\n  * [1. Directory Structure as Routes](#1-directory-structure-as-routes)\n  * [2. Key Design Decisions](#2-key-design-decisions)\n    + [Dynamic Routes](#dynamic-routes)\n    + [Hierarchical Middleware](#hierarchical-middleware)\n    + [Strict Route Endings](#strict-route-endings)\n- [Opinions in the Code](#opinions-in-the-code)\n- [Potential Issues and Considerations](#potential-issues-and-considerations)\n  * [1. Trailing Slash Handling](#1-trailing-slash-handling)\n  * [2. Middleware Control](#2-middleware-control)\n- [Best Practices When Using This System](#best-practices-when-using-this-system)\n- [Future Considerations](#future-considerations)\n- [API](#api)\n  * [Functions](#functions)\n  * [isMiddlewareFile(entry) ⇒ boolean](#ismiddlewarefileentry-%E2%87%92-boolean)\n  * [autoBox(ary) ⇒ Array](#autoboxary-%E2%87%92-array)\n  * [replaceUrlPlaceholders(urlPath) ⇒ string](#replaceurlplaceholdersurlpath-%E2%87%92-string)\n  * [isPlaceholder(urlPath) ⇒ boolean](#isplaceholderurlpath-%E2%87%92-boolean)\n  * [validatePath(path)](#validatepathpath)\n  * [dictionaryKeyStartsWithPath(dictionary, path) ⇒ Array.\u0026lt;function()\u0026gt;](#dictionarykeystartswithpathdictionary-path-%E2%87%92-arrayltfunctiongt)\n  * [curryObjectMethods(router, urlPath, ...initialMiddleWareFunctions) ⇒ Object](#curryobjectmethodsrouter-urlpath-initialmiddlewarefunctions-%E2%87%92-object)\n  * [buildMiddlewareDictionary(basePath, baseURL, [options]) ⇒ Object.\u0026lt;string, Array.\u0026lt;function()\u0026gt;\u0026gt;](#buildmiddlewaredictionarybasepath-baseurl-options-%E2%87%92-objectltstring-arrayltfunctiongtgt)\n  * [buildRoutes(basePath, baseURL) ⇒ Array.\u0026lt;Array.\u0026lt;string\u0026gt;\u0026gt;](#buildroutesbasepath-baseurl-%E2%87%92-arrayltarrayltstringgtgt)\n  * [composeRoutes(express, routeMappings, [options]) ⇒ Object](#composeroutesexpress-routemappings-options-%E2%87%92-object)\n- [Usage](#usage)\n- [Contributing](#contributing)\n  * [Rules](#rules)\n  * [Commit Message](#commit-message)\n  * [Testing](#testing)\n- [License](#license)\n- [Acknowledgments](#acknowledgments)\n  * [Dependencies](#dependencies)\n  * [Development Dependencies](#development-dependencies)\n\n\u003c!-- tocstop --\u003e\n\n## Features\n\n- 📁 **File-System Based Routing** - Automatically generates Express.js routes from your directory structure, similar to Next.js ( Convention over Configuration )\n- 🔄 **Dynamic Route Parameters** - Supports dynamic route parameters using `[paramName]` syntax that converts to Express.js `:paramName` format\n- 🔗 **Hierarchical Middleware** - Middleware cascades down from parent to child routes automatically\n- 🎯 **Convention over Configuration** - Follows clear conventions with `_middleware.js` and `index.js` files\n- 🔒 **Strict URL Handling** - Enforces consistent URL patterns with trailing slashes for better route organization\n- 🎨 **Clean API Design** - Simple and intuitive API that requires minimal setup\n- 🛠️ **Flexible Middleware Management**:\n  - Global middleware at root level\n  - Route-specific middleware at any level\n  - Support for multiple middleware functions per route\n  - Middleware execution order preservation\n- 📦 **Zero External Dependencies** - Only requires Express.js as a peer dependency\n- 🔍 **Type-Safe Path Parameters** - Directory names define your route parameters, ensuring consistency\n- 🎮 **Full HTTP Method Support** - Works with all Express HTTP methods (GET, POST, PUT, PATCH, DELETE)\n- ⚡ **Performance Optimized** - Routes are compiled at startup, not on each request\n- 🧪 **Testing Friendly** - Easy to test with clear route structure and middleware organization\n\n\n## Installation\n\n\u003c!--START_SECTION:file:INSTALLATION.md--\u003e\n**NPM**\n\n```shell\nnpm install @psenger/express-auto-router --save\n```\n**YARN**\n\n```shell\nyarn add @psenger/express-auto-router\n```\n\n\u003c!--END_SECTION:file:INSTALLATION.md--\u003e\n\n## How it Works\n\n\u003c!--START_SECTION:file:HOWITWORKS.md--\u003e\n\nExpress Auto Router is an elegant solution that transforms your directory structure into a fully functional Express.js routing system. It follows the philosophy of \"convention over configuration\" similar to Next.js and Nuxt.js, but for Express.js backend applications.\n\n### 1. Directory Structure as Routes\nThe system uses your file system structure to automatically generate Express routes. For example:\n```\nroutes/\n  ├── _middleware.js         # Global middleware\n  ├── users/\n  │   ├── _middleware.js     # Users-specific middleware\n  │   ├── index.js           # /users/ endpoint\n  │   └── [id]/              # Dynamic parameter\n  │       ├── _middleware.js # User-specific middleware\n  │       └── index.js       # /users/:id/ endpoint\n```\n\n### 2. Key Design Decisions\n\n#### Dynamic Routes\n- Uses a bracket notation `[paramName]` for dynamic route parameters\n- Automatically converts these to Express.js style parameters (`:paramName`)\n- Example: `/users/[userId]/posts/[postId]/` becomes `/users/:userId/posts/:postId/`\n\n#### Hierarchical Middleware\nOne of the most powerful features is the hierarchical middleware system:\n- Middleware cascades down from parent to child routes\n- Each directory can have its own `_middleware.js` file\n- Middleware is applied in order from most general to most specific\n- Example: A request to `/api/users/123/` will execute middleware in this order:\n  1. `/api/_middleware.js`\n  2. `/api/users/_middleware.js`\n  3. `/api/users/[id]/_middleware.js`\n\n#### Strict Route Endings\nAn opinionated decision in the code is the use of trailing slashes (`/`). The router is configured with `{ strict: true }`, which means:\n- All routes must end with a trailing slash\n- `/users` and `/users/` are treated as different routes\n- This is enforced throughout the system for consistency\n\n## Opinions in the Code\n\n1. **Strict Mode**\n```javascript\nconst routerOptions = options.routerOptions || { strict: true }\n```\nThe code enforces strict mode by default, treating trailing slashes as significant.\n\n2. **Middleware File Naming**\n```javascript\nexport function isMiddlewareFile(entry) {\n  return entry.isFile() \u0026\u0026 entry.name === '_middleware.js'\n}\n```\nThe system expects middleware files to be named exactly `_middleware.js`.\n\n3. **Hierarchical Middleware Organization**\nThe `dictionaryKeyStartsWithPath` function enforces a hierarchical middleware structure, sorting by path length to ensure proper execution order. Please note this is an opinion of how middleware should work and is baked into this system. If you want to control this it would have to be done inside the middleware.\n\n4. **Parameter calls**\n\nGlobal parameters/options can be passed to the controllers and middleware like this\n\n```javascript\n\nconst middlewareOptions = { logLevel: debug }\nconst controllerOptions = { env: 'test' }\ncomposeRoutes(express, routeMappings, { middlewareOptions, controllerOptions } )\n```\n\nYou should write your Controllers like this.\n\n```javascript\nmodule.exports = ( router, controllerOptions ) =\u003e {\n  ...\n  return router\n}\n```\n\nYou should write your Middleware like this.\n\n```javascript\nmodule.exports = ( middlewareOptions ) =\u003e {\n  return [\n    ...\n  ]\n}\n```\n\n## Potential Issues and Considerations\n\n### 1. Trailing Slash Handling\nThe biggest potential issue is the strict trailing slash requirement:\n\n**Pros:**\n- Consistent URL structure\n- Clear distinction between directories and files\n- Prevents double-slash issues\n\n**Cons:**\n- Many load balancers and Nginx configurations may strip trailing slashes\n- Can cause issues with some CDNs\n- May require additional configuration in reverse proxies\n\n**Mitigation Strategies:**\n1. Configure Nginx to preserve trailing slashes:\n```nginx\nlocation / {\n    proxy_pass http://backend;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header Host $http_host;\n    proxy_set_header X-NginX-Proxy true;\n    proxy_redirect off;\n}\n```\n\n2. Use URL rewriting rules to add trailing slashes if missing\n\n### 2. Middleware Control\nWhile the hierarchical middleware system is powerful, it can lead to unexpected behavior if not carefully managed. The code allows for fine-grained control through:\n\n1. Selective middleware application\n2. Middleware can check the route and choose to skip processing\n3. Order of middleware is predictable based on path depth\n\n## Best Practices When Using This System\n\n1. **Consistent Route Structure**\n   - Always use trailing slashes in your routes\n   - Keep route parameters in brackets: `[paramName]`\n   - Use descriptive parameter names\n\n2. **Middleware Organization**\n   - Place shared middleware at the highest appropriate level\n   - Use middleware selectively - don't add unnecessary layers\n   - Consider performance implications of deeply nested routes\n\n3. **Error Handling**\n   - Implement error handling middleware at the appropriate levels\n   - Use the hierarchical structure to catch errors at the right scope\n\n4. **Testing**\n   - Test routes with and without trailing slashes\n   - Verify middleware execution order\n   - Test dynamic parameter handling\n\n## Future Considerations\n\n1. **Optional Strict Mode**\n   - Consider making the strict trailing slash behavior configurable\n   - Add options for automatic slash handling\n\n2. **Middleware Enhancement**\n   - Add support for middleware priority ordering\n   - Implement middleware bypass options\n   - Add middleware execution tracking for debugging\n\n3. **Performance Optimization**\n   - Cache route compilation results\n   - Implement lazy loading for large route trees\n   - Add route validation at startup\n\nThis system provides a powerful and elegant solution for Express.js routing, but users should be aware of its opinions and potential infrastructure considerations, particularly regarding the trailing slash requirement.\n\n\u003c!--END_SECTION:file:HOWITWORKS.md--\u003e\n\n\u003c!--START_SECTION:jsdoc--\u003e\n## API\n\n### Functions\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eGlobal\u003c/th\u003e\u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#isMiddlewareFile\"\u003eisMiddlewareFile(entry)\u003c/a\u003e ⇒ \u003ccode\u003eboolean\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eChecks if a directory entry is a middleware file\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#autoBox\"\u003eautoBox(ary)\u003c/a\u003e ⇒ \u003ccode\u003eArray\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eEnsures a value is always an array by wrapping non-array values\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#replaceUrlPlaceholders\"\u003ereplaceUrlPlaceholders(urlPath)\u003c/a\u003e ⇒ \u003ccode\u003estring\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eConverts URL placeholder syntax [param] to Express parameter syntax :param\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#isPlaceholder\"\u003eisPlaceholder(urlPath)\u003c/a\u003e ⇒ \u003ccode\u003eboolean\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eChecks if a URL path contains a placeholder\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#validatePath\"\u003evalidatePath(path)\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eValidates if a path is a non-empty string\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#dictionaryKeyStartsWithPath\"\u003edictionaryKeyStartsWithPath(dictionary, path)\u003c/a\u003e ⇒ \u003ccode\u003eArray.\u0026lt;function()\u0026gt;\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eRetrieves and sorts middleware functions that match a given path\nFinds all entries in the dictionary where the given path starts with the dictionary key,\nsorts them by key length (shortest first), and returns the flattened array of middleware functions\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#curryObjectMethods\"\u003ecurryObjectMethods(router, urlPath, ...initialMiddleWareFunctions)\u003c/a\u003e ⇒ \u003ccode\u003eObject\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eCreates a curried router object with pre-configured URL path and middleware\nReturns a proxy to the original router that applies the given URL path and middleware functions\nto all HTTP method calls (get, post, put, etc.) automatically\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#buildMiddlewareDictionary\"\u003ebuildMiddlewareDictionary(basePath, baseURL, [options])\u003c/a\u003e ⇒ \u003ccode\u003eObject.\u0026lt;string, Array.\u0026lt;function()\u0026gt;\u0026gt;\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eBuilds a dictionary of middleware functions from a directory structure\nRecursively scans the given directory for \u0026#39;_middleware.js\u0026#39; files and builds a dictionary\nmapping URL paths to their corresponding middleware functions\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#buildRoutes\"\u003ebuildRoutes(basePath, baseURL)\u003c/a\u003e ⇒ \u003ccode\u003eArray.\u0026lt;Array.\u0026lt;string\u0026gt;\u0026gt;\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eBuilds an array of route mappings from a directory structure\nRecursively scans the given directory for \u0026#39;index.js\u0026#39; files and builds an array of\nURL paths and their corresponding file paths, converting directory placeholders to Express params\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#composeRoutes\"\u003ecomposeRoutes(express, routeMappings, [options])\u003c/a\u003e ⇒ \u003ccode\u003eObject\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eComposes Express routes from a directory structure with middleware support.\nThis is the main function that processes route mappings, builds middleware dictionaries,\nand configures an Express router with all discovered routes and middleware.\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ca name=\"isMiddlewareFile\"\u003e\u003c/a\u003e\n\n### isMiddlewareFile(entry) ⇒ \u003ccode\u003eboolean\u003c/code\u003e\nChecks if a directory entry is a middleware file\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eboolean\u003c/code\u003e - - True if the entry is a file named '_middleware.js'  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| entry | \u003ccode\u003eObject\u003c/code\u003e | The directory entry to check (fs.Dirent object) |\n\n**Example**  \n```js\n// With a file entry for '_middleware.js'\nconst middlewareEntry = { isFile: () =\u003e true, name: '_middleware.js' };\nisMiddlewareFile(middlewareEntry); // Returns: true\n```\n**Example**  \n```js\n// With a directory entry\nconst dirEntry = { isFile: () =\u003e false, name: '_middleware.js' };\nisMiddlewareFile(dirEntry); // Returns: false\n```\n**Example**  \n```js\n// With a different file\nconst otherFileEntry = { isFile: () =\u003e true, name: 'index.js' };\nisMiddlewareFile(otherFileEntry); // Returns: false\n```\n\u003ca name=\"autoBox\"\u003e\u003c/a\u003e\n\n### autoBox(ary) ⇒ \u003ccode\u003eArray\u003c/code\u003e\nEnsures a value is always an array by wrapping non-array values\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eArray\u003c/code\u003e - - Wraps the value in an array, or if the input was an array already it will return it as is.  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| ary | \u003ccode\u003e\\*\u003c/code\u003e | The value to convert to an array |\n\n**Example**  \n```js\n// With a non-array value\nautoBox(5); // Returns: [5]\n```\n**Example**  \n```js\n// With an array value\nautoBox([1, 2, 3]); // Returns: [1, 2, 3]\n```\n**Example**  \n```js\n// With null or undefined\nautoBox(null); // Returns: [null]\nautoBox(undefined); // Returns: [undefined]\n```\n**Example**  \n```js\n// With an object\nautoBox({ key: 'value' }); // Returns: [{ key: 'value' }]\n```\n\u003ca name=\"replaceUrlPlaceholders\"\u003e\u003c/a\u003e\n\n### replaceUrlPlaceholders(urlPath) ⇒ \u003ccode\u003estring\u003c/code\u003e\nConverts URL placeholder syntax [param] to Express parameter syntax :param\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003estring\u003c/code\u003e - - The URL path with Express-style parameters  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| urlPath | \u003ccode\u003estring\u003c/code\u003e | The URL path containing placeholders |\n\n**Example**  \n```js\n// With single placeholder\nreplaceUrlPlaceholders('/users/[id]'); // Returns: '/users/:id'\n```\n**Example**  \n```js\n// With multiple placeholders\nreplaceUrlPlaceholders('/users/[id]/posts/[postId]'); // Returns: '/users/:id/posts/:postId'\n```\n**Example**  \n```js\n// With no placeholders\nreplaceUrlPlaceholders('/users/list'); // Returns: '/users/list'\n```\n**Example**  \n```js\n// With nested/complex placeholders\nreplaceUrlPlaceholders('/products/[category]/[id]/reviews/[reviewId]');\n// Returns: '/products/:category/:id/reviews/:reviewId'\n```\n\u003ca name=\"isPlaceholder\"\u003e\u003c/a\u003e\n\n### isPlaceholder(urlPath) ⇒ \u003ccode\u003eboolean\u003c/code\u003e\nChecks if a URL path contains a placeholder\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eboolean\u003c/code\u003e - - True if the path contains a placeholder  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| urlPath | \u003ccode\u003estring\u003c/code\u003e | The URL path to check |\n\n**Example**  \n```js\n// With placeholder\nisPlaceholder('/users/[id]'); // Returns: true\n```\n**Example**  \n```js\n// With multiple placeholders\nisPlaceholder('/users/[id]/posts/[postId]'); // Returns: true\n```\n**Example**  \n```js\n// Without placeholder\nisPlaceholder('/users/list'); // Returns: false\n```\n**Example**  \n```js\n// With square brackets in a different context (not a placeholder)\nisPlaceholder('/users/list[all]'); // Returns: true (matches the regex pattern)\n```\n\u003ca name=\"validatePath\"\u003e\u003c/a\u003e\n\n### validatePath(path)\nValidates if a path is a non-empty string\n\n**Kind**: global function  \n**Throws**:\n\n- \u003ccode\u003eError\u003c/code\u003e If path is not a string or is empty\n\n\n| Param | Type | Description |\n| --- | --- | --- |\n| path | \u003ccode\u003estring\u003c/code\u003e | The path to validate |\n\n**Example**  \n```js\n// With valid path\nvalidatePath('/api/users'); // No error thrown\n```\n**Example**  \n```js\n// With empty string\ntry {\n  validatePath('');\n} catch (error) {\n  console.error(error.message); // Outputs: 'Invalid path provided'\n}\n```\n**Example**  \n```js\n// With null value\ntry {\n  validatePath(null);\n} catch (error) {\n  console.error(error.message); // Outputs: 'Invalid path provided'\n}\n```\n**Example**  \n```js\n// With non-string value\ntry {\n  validatePath(123);\n} catch (error) {\n  console.error(error.message); // Outputs: 'Invalid path provided'\n}\n```\n\u003ca name=\"dictionaryKeyStartsWithPath\"\u003e\u003c/a\u003e\n\n### dictionaryKeyStartsWithPath(dictionary, path) ⇒ \u003ccode\u003eArray.\u0026lt;function()\u0026gt;\u003c/code\u003e\nRetrieves and sorts middleware functions that match a given path\nFinds all entries in the dictionary where the given path starts with the dictionary key,\nsorts them by key length (shortest first), and returns the flattened array of middleware functions\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eArray.\u0026lt;function()\u0026gt;\u003c/code\u003e - - Array of middleware functions that apply to the path, ordered by path specificity  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| dictionary | \u003ccode\u003eObject.\u0026lt;string, (function()\\|Array.\u0026lt;function()\u0026gt;)\u0026gt;\u003c/code\u003e | Dictionary of paths to middleware functions |\n| path | \u003ccode\u003estring\u003c/code\u003e | The path to match |\n\n**Example**  \n```js\n// With matching paths\nconst dict = {\n  '/api/': [authMiddleware],\n  '/api/users/': [userMiddleware]\n};\ndictionaryKeyStartsWithPath(dict, '/api/users/profile');\n// Returns: [authMiddleware, userMiddleware] (in order from least to most specific)\n```\n**Example**  \n```js\n// With no matching paths\nconst dict = {\n  '/api/': [authMiddleware],\n  '/api/users/': [userMiddleware]\n};\ndictionaryKeyStartsWithPath(dict, '/admin/');\n// Returns: []\n```\n**Example**  \n```js\n// With mixed array and single function values\nconst dict = {\n  '/api/': [authMiddleware, logMiddleware],\n  '/api/users/': userMiddleware\n};\ndictionaryKeyStartsWithPath(dict, '/api/users/');\n// Returns: [authMiddleware, logMiddleware, userMiddleware]\n```\n**Example**  \n```js\n// With null or undefined values in the dictionary (they are filtered out)\nconst dict = {\n  '/api/': [authMiddleware, null],\n  '/api/users/': undefined\n};\ndictionaryKeyStartsWithPath(dict, '/api/users/');\n// Returns: [authMiddleware]\n```\n\u003ca name=\"curryObjectMethods\"\u003e\u003c/a\u003e\n\n### curryObjectMethods(router, urlPath, ...initialMiddleWareFunctions) ⇒ \u003ccode\u003eObject\u003c/code\u003e\nCreates a curried router object with pre-configured URL path and middleware\nReturns a proxy to the original router that applies the given URL path and middleware functions\nto all HTTP method calls (get, post, put, etc.) automatically\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eObject\u003c/code\u003e - - Curried router proxy with pre-configured path and middleware  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| router | \u003ccode\u003eObject\u003c/code\u003e | Express router instance |\n| urlPath | \u003ccode\u003estring\u003c/code\u003e | The URL path to be curried |\n| ...initialMiddleWareFunctions | \u003ccode\u003efunction\u003c/code\u003e | Initial middleware functions to be applied (rest parameter, accepts multiple functions) |\n\n**Example**  \n```js\n// Basic usage with a single middleware function\nconst router = express.Router();\nconst curriedRouter = curryObjectMethods(router, '/users', authMiddleware);\ncurriedRouter.get((req, res) =\u003e res.json({}));\n// Equivalent to: router.get('/users', authMiddleware, (req, res) =\u003e res.json({}));\n```\n**Example**  \n```js\n// With multiple middleware functions\nconst curriedRouter = curryObjectMethods(router, '/posts', authMiddleware, logMiddleware);\ncurriedRouter.post((req, res) =\u003e res.status(201).json({}));\n// Equivalent to: router.post('/posts', authMiddleware, logMiddleware, (req, res) =\u003e res.status(201).json({}));\n```\n**Example**  \n```js\n// With no middleware\nconst curriedRouter = curryObjectMethods(router, '/public');\ncurriedRouter.get((req, res) =\u003e res.send('Hello'));\n// Equivalent to: router.get('/public', (req, res) =\u003e res.send('Hello'));\n```\n**Example**  \n```js\n// Accessing the original router object\nconst curriedRouter = curryObjectMethods(router, '/api');\nconst originalRouter = curriedRouter._getOriginalObject();\n// originalRouter is the router instance passed in the first parameter\n```\n\u003ca name=\"buildMiddlewareDictionary\"\u003e\u003c/a\u003e\n\n### buildMiddlewareDictionary(basePath, baseURL, [options]) ⇒ \u003ccode\u003eObject.\u0026lt;string, Array.\u0026lt;function()\u0026gt;\u0026gt;\u003c/code\u003e\nBuilds a dictionary of middleware functions from a directory structure\nRecursively scans the given directory for '_middleware.js' files and builds a dictionary\nmapping URL paths to their corresponding middleware functions\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eObject.\u0026lt;string, Array.\u0026lt;function()\u0026gt;\u0026gt;\u003c/code\u003e - Dictionary where keys are URL paths and values are arrays of middleware functions  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| basePath | \u003ccode\u003estring\u003c/code\u003e | Base filesystem path to start scanning |\n| baseURL | \u003ccode\u003estring\u003c/code\u003e | Base URL path for the routes |\n| [options] | \u003ccode\u003eObject\u003c/code\u003e | Options that can be passed to all controllers when they are executed. |\n\n**Example**  \n```js\n// Basic directory structure with middleware\n// ./src/routes/_middleware.js         -\u003e exports a global middleware\n// ./src/routes/users/_middleware.js   -\u003e exports a users-specific middleware\nconst middlewares = buildMiddlewareDictionary('./src/routes', '/api');\n// Returns: {\n//   '/api/': [globalMiddleware],\n//   '/api/users/': [usersMiddleware]\n// }\n```\n**Example**  \n```js\n// With dynamic route parameters\n// ./src/routes/users/[id]/_middleware.js  -\u003e exports a user-specific middleware\nconst middlewares = buildMiddlewareDictionary('./src/routes', '/api');\n// Returns: {\n//   '/api/': [globalMiddleware],\n//   '/api/users/': [usersMiddleware],\n//   '/api/users/:id/': [userSpecificMiddleware]\n// }\n```\n**Example**  \n```js\n// With middleware exporting multiple functions\n// ./src/routes/_middleware.js  -\u003e exports [authMiddleware, logMiddleware]\nconst middlewares = buildMiddlewareDictionary('./src/routes', '/api');\n// Returns: {\n//   '/api/': [authMiddleware, logMiddleware]\n// }\n```\n**Example**  \n```js\n// With middleware exporting a single function\n// ./src/routes/_middleware.js  -\u003e exports singleMiddleware (not in an array)\nconst middlewares = buildMiddlewareDictionary('./src/routes', '/api');\n// Returns: {\n//   '/api/': [singleMiddleware]\n// }\n```\n\u003ca name=\"buildRoutes\"\u003e\u003c/a\u003e\n\n### buildRoutes(basePath, baseURL) ⇒ \u003ccode\u003eArray.\u0026lt;Array.\u0026lt;string\u0026gt;\u0026gt;\u003c/code\u003e\nBuilds an array of route mappings from a directory structure\nRecursively scans the given directory for 'index.js' files and builds an array of\nURL paths and their corresponding file paths, converting directory placeholders to Express params\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eArray.\u0026lt;Array.\u0026lt;string\u0026gt;\u0026gt;\u003c/code\u003e - Array of tuples where first element is URL path and second is file path  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| basePath | \u003ccode\u003estring\u003c/code\u003e | Base filesystem path to start scanning |\n| baseURL | \u003ccode\u003estring\u003c/code\u003e | Base URL path for the routes |\n\n**Example**  \n```js\n// Basic directory structure\n// ./src/routes/users/index.js\n// ./src/routes/posts/index.js\nconst routes = buildRoutes('./src/routes', '/api');\n// Returns: [\n//   ['/api/users/', './src/routes/users/index.js'],\n//   ['/api/posts/', './src/routes/posts/index.js']\n// ]\n```\n**Example**  \n```js\n// With dynamic route parameters\n// ./src/routes/users/[id]/index.js\nconst routes = buildRoutes('./src/routes', '/api');\n// Returns: [\n//   ['/api/users/:id/', './src/routes/users/[id]/index.js']\n// ]\n```\n**Example**  \n```js\n// With nested dynamic routes\n// ./src/routes/users/[userId]/posts/[postId]/index.js\nconst routes = buildRoutes('./src/routes', '/api');\n// Returns: [\n//   ['/api/users/:userId/posts/:postId/', './src/routes/users/[userId]/posts/[postId]/index.js']\n// ]\n```\n**Example**  \n```js\n// With root route\n// ./src/routes/index.js\nconst routes = buildRoutes('./src/routes', '/api');\n// Returns: [\n//   ['/api/', './src/routes/index.js']\n// ]\n```\n\u003ca name=\"composeRoutes\"\u003e\u003c/a\u003e\n\n### composeRoutes(express, routeMappings, [options]) ⇒ \u003ccode\u003eObject\u003c/code\u003e\nComposes Express routes from a directory structure with middleware support.\nThis is the main function that processes route mappings, builds middleware dictionaries,\nand configures an Express router with all discovered routes and middleware.\n\n**Kind**: global function  \n**Returns**: \u003ccode\u003eObject\u003c/code\u003e - Configured Express router with applied routes  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| express | \u003ccode\u003eObject\u003c/code\u003e | The Express module instance |\n| routeMappings | \u003ccode\u003eArray.\u0026lt;Object\u0026gt;\u003c/code\u003e | Array of route mapping configurations |\n| routeMappings[].basePath | \u003ccode\u003estring\u003c/code\u003e | Base filesystem path to start scanning |\n| routeMappings[].baseURL | \u003ccode\u003estring\u003c/code\u003e | Base URL path for the routes |\n| [options] | \u003ccode\u003eObject\u003c/code\u003e | Configuration options |\n| [options.routerOptions] | \u003ccode\u003eObject\u003c/code\u003e | Options for the Express router (default: `{ strict: true }` stay with this for best results but be advised it makes paths require to be terminated with `/` ) |\n| [options.middlewareOptions] | \u003ccode\u003eObject\u003c/code\u003e | Options passed to every middleware. |\n| [options.controllerOptions] | \u003ccode\u003eObject\u003c/code\u003e | Options passed to every controller. |\n\n**Example**  \n```js\n// Basic usage with a single route mapping\nconst express = require('express');\nconst app = express();\n\nconst router = composeRoutes(express, [\n  {\n    basePath: './src/routes',\n    baseURL: '/api'\n  }\n]);\n\napp.use(router);\n// This will set up all routes found in './src/routes' with their middleware\n```\n**Example**  \n```js\n// With multiple route mappings\nconst router = composeRoutes(express, [\n  {\n    basePath: './src/api/routes',\n    baseURL: '/api'\n  },\n  {\n    basePath: './src/admin/routes',\n    baseURL: '/admin'\n  }\n]);\n```\n**Example**  \n```js\n// With custom router options\nconst router = composeRoutes(express, [\n  {\n    basePath: './src/routes',\n    baseURL: '/api'\n  }\n], {\n  routerOptions: {\n    strict: true,\n  }\n});\n```\n**Example**  \n```js\n// With an existing router instance\nconst existingRouter = express.Router();\nconst router = composeRoutes(express, [\n  {\n    basePath: './src/routes',\n    baseURL: '/api'\n  }\n], {\n  router: existingRouter\n});\n```\n\n\u003c!--END_SECTION:jsdoc--\u003e\n\n## Usage\n\n\u003c!--START_SECTION:file:USAGE.md--\u003e\nLets make some assumptions, and then walk through what will happen.\n\n1. your Routes should look something like this.\n\n```\nsrc/routes\n├── closed\n│   └── organizations\n│       └── [organizationId]\n│           ├── clients\n│           │   └── [clientId]\n│           │       ├── contracts\n│           │       │   └── index.js\n│           │       └── projects\n│           │           └── index.js\n│           └── departments\n│               └── [departmentId]\n│                   ├── employees\n│                   │   ├── [employeeId]\n│                   │   │   ├── projects\n│                   │   │   │   └── index.js\n│                   │   │   └── tasks\n│                   │   │       └── index.js\n│                   │   └── index.js\n│                   └── subdepartments\n│                       └── [subDepartmentId]\n│                           └── employees\n│                               ├── [employeeId]\n│                               │   ├── projects\n│                               │   │   └── index.js\n│                               │   └── tasks\n│                               │       └── index.js\n│                               └── index.js\n└── open\n    ├── _middleware.js\n    ├── blog-posts\n    │   ├── [blogPostId]\n    │   │   └── index.js\n    │   ├── _middleware.js\n    │   └── index.js\n    └── users\n        ├── [userId]\n        │   ├── blog-posts\n        │   │   ├── _middleware.js\n        │   │   └── index.js\n        │   ├── friends\n        │   │   ├── [friendId]\n        │   │   │   └── blog-posts\n        │   │   │       ├── _middleware.js\n        │   │   │       └── index.js\n        │   │   └── index.js\n        │   └── index.js\n        └── index.js\n\n```\n\nThis program will scan a directory structure and build URLs and Path Parameters based on the following rules:\n\n- path parameters can only be a directory\n- path parameters are identified as a bracket ( `[` ) followed by some text that can be a valid Express path parameter\n  and then closed off by a closing bracket `]`\n- there can only be two files in a single directory, an `index.js` or `_middleware.js`\n- both of these javascript files, must return a function to be executed\n  - `_middleware.js` accepts no parameters, however this is to allow you to perform Dependency Injection (DI) for the\n    purpose of testing and isolation ( more on this later )\n    - The middleware is hieratical by default, using Regular Expressions to do this, makes a system unnecessarily more\n      complicated and impacts performance.\n  - `index.js` accepts a Express Router and is expected to return that router with controllers ( and local middleware )\n    attached to the router object.\n    - while in the `index.js` you will not need to provide a path, the code will do that for you, it will even convert\n      the path variables to Express Path variables.\n\n2. An example `index.js`\n\nA simple example without a local middleware\n\n```javascript\nconst standard_controllers = (req, res, _next) =\u003e res.status(200).send({\n  route: `${req.baseUrl}${req.route.path}`,\n  params: req.params\n})\n\nmodule.exports = (router) =\u003e {\n  router.get(standard_controllers)\n  router.post(standard_controllers)\n  router.put(standard_controllers)\n  router.patch(standard_controllers)\n  router.delete(standard_controllers)\n  return router\n}\n```\n\nAnother simple example with localized middleware ( it will only apply to requests made into this path )\n\n```javascript\nconst microMiddleware = (req, res, next) =\u003e {\n  req.params = req.params || {};\n  req.params.context = req.params.context || {};\n  Object.assign(req.params.context, { microMiddleware: true })\n  next()\n}\n\nconst standard_controllers = (req, res, _next) =\u003e res.status(200).send({\n  route: `${req.baseUrl}${req.route.path}`,\n  params: req.params\n})\n\nmodule.exports = (router) =\u003e {\n  router.get(microMiddleware, standard_controllers)\n  router.post(microMiddleware, standard_controllers)\n  router.put(microMiddleware, standard_controllers)\n  router.patch(microMiddleware, standard_controllers)\n  router.delete(microMiddleware, standard_controllers)\n  return router\n}\n\n```\n\n3. An example `_middleware.js`\n\nA single middleware that will be applied to the current directory  ( end point ) and all subsequent paths.\n\n```javascript\nmodule.exports = () =\u003e {\n  function standard_middleware(req, res, next) {\n    req.params = req.params || {};\n    req.params.context = req.params.context || {};\n    // Merge the context object with req.params.context\n    Object.assign(req.params.context, { blogPost: true })\n    next()\n  }\n\n  return standard_middleware\n}\n\n```\n\nMultiple middleware, with importance on the order of execution, applied to the current directory ( end point ) and all\nsubsequent paths.\n\n```javascript\nmodule.exports = () =\u003e {\n  function standard_middleware_must_go_first(req, res, next) {\n    req.params = req.params || {};\n    req.params.context = req.params.context || {};\n    // Merge the context object with req.params.context\n    Object.assign(req.params.context, { first: true })\n    next()\n  }\n\n  function standard_middleware_must_go_second(req, res, next) {\n    req.params = req.params || {};\n    req.params.context = req.params.context || {};\n    if (req.params.context.first) {\n      Object.assign(req.params.context, { second: true })\n      return next()\n    }\n    next(new Error('Missing required first execution of the middleware'))\n  }\n\n  return [\n    standard_middleware_must_go_first,\n    standard_middleware_must_go_second\n  ]\n}\n```\n\n4. At this point, you are ready to use it...\n\n```javascript\n\nconst express = require('express')\nconst { join } = require('path')\nconst composeRoutes = require('@psenger/express-auto-router').default\nconst app = express()\nconst routeMappings = [\n  {\n    basePath: join(process.cwd(), 'src', 'routes', 'open'),\n    baseURL: '/open'\n  },\n  {\n    basePath: join(process.cwd(), 'src', 'routes', 'closed'),\n    baseURL: '/closed'\n  }\n]\napp.use('/api', composeRoutes(express, routeMappings))\nmodule.exports = app\n\n```\n\n\u003c!--END_SECTION:file:USAGE.md--\u003e\n\n## Contributing\n\n\u003c!--START_SECTION:file:CONTRIBUTING.md--\u003e\n\nThanks for contributing! 😁 Here are some rules that will make your change to\nmarkdown-fences fruitful.\n\n### Rules\n\n* Raise a ticket to the feature or bug can be discussed\n* Pull requests are welcome, but must be accompanied by a ticket approved by the repo owner\n* You are expected to add a unit test or two to cover the proposed changes.\n* Please run the tests and make sure tests are all passing before submitting your pull request\n* Do as the Romans do and stick with existing whitespace and formatting conventions (i.e., tabs instead of spaces, etc)\n  * we have provided the following: `.editorconfig` and `.eslintrc`\n  * Don't tamper with or change `.editorconfig` and `.eslintrc`\n* Please consider adding an example under examples/ that demonstrates any new functionality\n\n### Commit Message\n\nThis module uses [release-please](https://github.com/googleapis/release-please) which\nneeds commit messages to look like the following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)\n\n```\n\u003ctype\u003e[optional scope]: \u003cdescription\u003e\n\n[optional body]\n\n```\n\n**type** is typically `fix`, `feat`. When **type** ends with a `!` or is `BREAKING CHANGE` it indicates this is a breaking change.\n\n**type** should be followed by a short description, **\u003cdescription\u003e**\n\n**optional body** can have more detail\n\n### Testing\n\n* All tests are expected to work\n* Tests are based off of `dist/index.js` **NOT** your src code. Therefore, you should BUILD it first.\n* Coverage should not go down, and I acknowledge it is very difficult to get the tests to 100%\n\n\u003c!--END_SECTION:file:CONTRIBUTING.md--\u003e\n\n## License\n\n\u003c!--START_SECTION:file:LICENSE--\u003e\nMIT License\n\nCopyright (c) 2025 Philip A Senger\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\u003c!--END_SECTION:file:LICENSE--\u003e\n\n\u003c!--START_SECTION:file:THIRD_PARTY_NOTICES.md--\u003e\n\n## Acknowledgments\n\nThis project directly uses the following open-source packages:\n\n### Dependencies\n\n- None\n\n### Development Dependencies\n\n- [@psenger/markdown-fences](https://github.com/psenger/markdown-fences) - MIT License\n- [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) - MIT License\n- [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) - MIT License\n- [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) - MIT License\n- [eslint](https://github.com/eslint/eslint) - MIT License\n- [express](https://github.com/expressjs/express) - MIT License\n- [jest-html-reporters](https://github.com/Hazyzh/jest-html-reporters) - MIT License\n- [jest](https://github.com/jestjs/jest) - MIT License\n- [jsdoc](https://github.com/jsdoc/jsdoc) - Apache-2.0 License\n- [license-checker](https://github.com/davglass/license-checker) - BSD-3-Clause License\n- [markdown-toc](https://github.com/jonschlinkert/markdown-toc) - MIT License\n- [prettier](https://github.com/prettier/prettier) - MIT License\n- [rimraf](https://github.com/isaacs/rimraf) - ISC License\n- [rollup](https://github.com/rollup/rollup) - MIT License\n- [standard-version](https://github.com/conventional-changelog/standard-version) - ISC License\n- [supertest](https://github.com/ladjs/supertest) - MIT License\n\n\u003c!--END_SECTION:file:THIRD_PARTY_NOTICES.md--\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsenger%2Fexpress-auto-router","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpsenger%2Fexpress-auto-router","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsenger%2Fexpress-auto-router/lists"}