# Node File Router > A lightweight and fast file-based routing for Node.js. Works with any framework or pure Node.js HTTP server. This file contains all documentation content in a single document following the llmstxt.org standard. ## Getting Started The library creates a simple and intuitive method of routing your application's URLs based on your file and folder structure. ## Introduction This tutorial uses JavaScript and CommonJS module types to keep things simple and quick to get started. Ready-to-run examples with TypeScript, ECMAScript module types, and more can be found in the "examples" folder. ## Install 💿 Add `node-file-router` to your dependency list. npm install node-file-router yarn add node-file-router pnpm add node-file-router ### What you'll need - [Node.js](https://nodejs.org/en/download/) version 14 or above ## Initialization Create an `server.js` file and do the following: ```js // 1. Import default http module and node-file-router const http = require('node:http'); const { initFileRouter } = require('node-file-router'); // 2. Create an entry-point function async function run() { // 3. Initialize node-file-router and the handler function const useFileRouter = await initFileRouter(); const server = http.createServer((req, res) => { // 4. Create a server and invoke created function on each request useFileRouter(req, res); }); // 5. Start listening a server on 4000 port const port = 4000; server.listen(port, () => console.log(`Server running at http://localhost:${port}/`) ); } // 6. Run entry-point function run(); ``` ## A first route We will create a home route that displays welcome content. 1. Create an `api` folder at the root of your project. ``` ├── api/ <- ├── server.js └── package.json ``` 2. Create a file named `index.js` inside this folder: ``` ├── api/ │ └── index.js <- ├── server.js └── package.json ``` ```js module.exports = function index(req, res) { res.end("Welcome to our shop!"); } ``` 3. Run a server using: `node server.js` command 4. Open a browser and navigate to `http://localhost:4000`. You should see the message `Welcome to our shop!` displayed. #### Congratulations! 🎉 You've created a first file route ## Add HTTP Methods 1. Before we start, we need a small utility function which will parse json from a request. Create a folder `utils` and put `http.utils.js` file inside. ``` ├── api/ ├── ... ├── utils/ │ └── http.utils.js <- ... ``` ```js module.exports = { parseJson(request) { return new Promise((resolve, reject) => { let data = ''; request.on('data', (chunk) => { data += chunk; }); request.on('end', () => { try { const parsedData = JSON.parse(data); resolve(parsedData); } catch (e) { reject(e); } }); }); } } ``` 2. Create a folder called `products` and an `index.js` file inside it. ``` ├── api/ │ ├── products/ <- │ │ └── index.js <- │ └── ... ... ``` 2. Implement `get` and `post` methods: ```js const { parseJson } = require('../../utils/http.utils'); module.exports = { get: (req, res) => { res.end('list of products'); }, post: async (req, res) => { const newProduct = await parseJson(req); res.end(`a product will be created: ${JSON.stringify(newProduct)}`); } } ``` 3. Open a browser and go to http://localhost:4000/products. You should see a `list of products` message displayed. 4. Make a POST request using `curl`, `Postman` or any tool you like on `http://localhost:4000/products` The response should display `a product will be created` along with your content. #### Perfect! 🎉 Let's move on # Dynamic Routes 1. Create a new file with the name `[id]` inside the `product` folder. ``` ├── api/ │ ├── products/ │ │ ├── ... │ │ └── [id].js <- │ └── index.js ... ``` 2. Fill it the same way as you did before: ```js module.exports = { // Add the `routeParams` argument as the final argument to the function. This argument will contain // all the taken route parameters. get: (req, res, routeParams) => { const { id } = routeParams; res.end(`product ${id} info`); } }; ``` 3. Open a browser and go to `http://localhost:4000/products/123`. The page should display a message: `product 123`. Alright, let's make it more sophisticated. Say, we want to address the following case: `/catalog/tag-1/tag-2/tag-n` 4. Create a `catalog` folder with `[[...categories]].js` inside. ``` ├── api/ │ ├── catalog/ │ │ ├── ... │ │ └── [[...categories]].js │ ├── index.js ... ``` And add a single `get` method: ```js module.exports = { get: (req, res, routeParams) => { const { categories } = routeParams; // This type of route also covers just "/catalog" if (!categories) { return res.end('all products'); } res.end(`get products that have such tags: ${categories}`); }, }; ``` 5. Open a browser and go to `http://localhost:4000/catalog/men/sneakers/nike`. The page should display a list of categories: `'men','sneakers','nike'` #### 🥁 That's it! # What's Next? 1. [Discover more capabilities of the routing system](/docs/route-matching) 2. [Configuration and More Details](/docs/route-matching) 3. [Examples with Express, WebSockets, ECMAScript, and more](https://github.com/Danilqa/node-file-router/tree/main/examples) 4. [How to create your own custom adapter and use it with your favorite framework](/docs/custom-adapter) --- ## Usage Guide ## Initialization 1. To initialize all routes, invoke the `initFileRouter` function. This is an asynchronous function that constructs a table of URL patterns and creates a function that calls the matched file. 2. Call the created function on each request, passing in the `request`, `response`, and any other necessary parameters. ```js const useFileRouter = await initFileRouter(); const server = http.createServer((req, res) => { useFileRouter(req, res); // ... your code below }); ``` ```js const app = express(); const fileRouter = await initFileRouter(); app.use(fileRouter); app.listen(4004); ``` :::tip Note The information described below pertains only to the default Node.js incoming/outgoing message interfaces, and works for both the default HTTP server and Express.js. If you use sockets, a different framework, etc., you will need to create a [custom adapter](/docs/custom-adapter). ::: For each incoming HTTP request, the library takes the URL and attempts to match it with a corresponding file in your application. For example, a request with the URL `/users/123/orders/456` will invoke the `/users/[productId]/orders/[orderId].js` file. :::tip Great news! It works with any popular types: `ts`, `js`, `cjs`, and `mjs`. ::: ## Configuration ### Source of Files **name**: `baseDir`, **default value**: `./api` Set the destination path for the folder that the library will scan during requests. By default, the `api` folder is used. ```js const useFileRouter = await initFileRouter({ baseDir: 'routes' }); ``` The relative path is related to the current working directory of the Node.js process. ### Ignoring Files by Pattern **name**: `ignoreFilesRegex`, **default value**: `[]` To ignore certain folders and files, you can use regular expressions in the `ignoreFilesRegex` property. This is useful for ignoring tests or any internal functionality that you don't want to include. ```js const useFileRouter = await initFileRouter({ ignoreFilesRegex: [/^_.*$/, /.\.some-spec/] }); ``` ### Clear Import Cache **name**: `cleanImportCache`, **default value**: `false`\ **env:** works only with CommonJS modules for now By default, Node.js caches the imported files. This means that if you change route handler files during runtime, the changes will not be applied even if you initialize the file router. To avoid this, you can enable the `clearImportCache` option and reinitialize the router. It will clean the cache before importing a file. For example: ```js let useFileRouter = await initFileRouter(); const server = http.createServer(async (req, res) => { if (req.url === '/refresh') { useFileRouter = await initFileRouter({ clearImportCache: true }); res.end('done'); return; } useFileRouter(req, res); }); ``` ## Methods Routing The library offers various options for routing methods. :::tip Note Anything that is passed into useFileRouter will not be mutated. Route parameters are always passed as the last argument. For instance, in Express.js, it will be: `function(req, res, next, routeParams)`. ::: ### Any Method The function will be invoked for any request method, such as `GET`, `POST`, `PATCH`, and so on. ```js export default function(req, res, routeParams) { // Take dynamic route params from the last argument } ``` Get the route params from dynamic paths in the last argument. For instance, in this file `/users/[productId]/orders/[orderId].js` these params are available: `{ productId, orderId }`. More details are described on the [route matching part](/docs/route-matching). ### Object with Methods Specify functions for each method in the object. The rules for nested functions are identical, with the only difference being that they belong to the method. Any method that does not list a function will result in a 404 error. ```js export default { get(req, res, routeParams) { const { documentId, draftId } = routeParams; res.end(`Requested document ${documentId} and his draft ${draftId}`); }, post(req, res, routeParams) { const { documentId, draftId } = routeParams; res.end(`Created draft ${draftId} for document ${documentId}`); }, patch(req, res, routeParams) { // ... }, } ``` ### Class with Methods Specify functions for each method within the class. The main requirement is that it should be the default export. ```ts export default class SomeResource { get(req, res, routeParams) { const { documentId, draftId } = routeParams; res.end(`Requested document ${documentId} and his draft ${draftId}`); } patch(req, res, routeParams) { // ... } } ``` Alternatively, you can export a class instance: ```ts class SomeResource { get(req, res, routeParams) { // ... } } export default new SomeResource(); ``` ### Methods in Filenames Use the `.[method].` suffix to directly specify methods in the filenames. This approach can be used for all types of routes. Plain routes: ``` ├── user/ │ ├── login.[post].js │ └── logout.[post].js ... ``` Exact slug: ``` ├── drafts/ │ ├── [id].[post].js │ └── [id].[get].js ... ``` Catch all: ``` ├── catalog/ │ └── [...categories].[get].js ... ``` Optional catch all: ``` ├── tags/ │ └── [[...ids]].[post].js ... ``` Index files: ``` ├── auth/ │ └── index.[post].js ... ``` The content of the file is represented as a function: ```js export default function(req, res, routeParams) { // ... your code } ``` ## Get All Registered Routes Get all parsed routes for cases where you need to show or process all routes after initialization. The initialization function `initFileRouter` receives lifecycle callback `onInit` which has `routes`. ```ts await initFileRouter({ onInit: ({ routes }) => { console.log({ routes }) } }); ``` The list contains the following items: ```ts interface Route { // Formatted endpoint path // Example: "shops/[shopId]/products/[productId]" urlPath: string; // Contains the request handler functions from the file // For instance, // - for single handler: // (req, res, routeParams) => {} // - for method-based handler: // { // get: (req, res, routeParams) => {}, // post: (req, res, routeParams) => {} // } handler: RequestHandler | Record; // When a file has a specified method in its name, like suppliers.[get].ts // Example: 'get', 'post', 'put', etc. method?: string; // The filename // Example: suppliers.[get].ts fileName: string; // Regexp which is used to match URL on file // Example: ^\/one\/(?[^/]+)\/two\/(?[^/]+)\/?$ regex: RegExp; } ``` --- ## Usage with Bun [Bun](https://bun.sh/) is a toolkit for JavaScript and TypeScript development, allowing you to run, build, test, and debug a full-stack application. For examples of projects, please refer to these resources: [Bun](https://github.com/Danilqa/node-file-router/tree/main/examples/bun) or [Bun TypeScript](https://github.com/Danilqa/node-file-router/tree/main/examples/bun-typescript). ## Initialization 1\. Set up an entry point: ```js // 1. Import node-file-router // 2. Initialize the main request handler const useFileRouter = await initFileRouter(); // 3. Start serving server and attach request handler to fetch const server = Bun.serve({ port: 3123, fetch: (req) => useFileRouter(req) }); console.log(`Listening on http://localhost:${server.port}`); ``` ```ts // 1. Import node-file-router // 2. Initialize the main request handler const useFileRouter = await initFileRouter(); // 3. Start serving server and attach request handler to fetch const server = Bun.serve({ port: 3123, fetch: async (req) => { const res = await useFileRouter(req); return res || new Response('No Response is provided', { status: 500 }); } }); console.log(`Listening on http://localhost:${server.port}`); ``` 2\. Override the default 404 handler: ``` ├── api/ │ └── _404.js ... ``` ```js export default function NotFound() { return new Response('404'); } ``` ## Handlers Implementation ### Basic Handler Bun operates with a `Response` object that should be returned. Therefore, the API handlers look like the following: ``` ├── api/ │ └── products.js ... ``` ```js export default function ProductsRoute(req) { return new Response('products'); } ``` ```ts export default function ProductsRoute(req: Request) { return new Response('products'); } ``` ### Request and Response ``` ├── api/users │ └── [id].js ... ``` Since we have a response on return, the route params object is the second argument. ```js const db = { 13: { id: '13', name: 'Dan' } } export default { async put(req, routeParams) { db[routeParams.id] = await req.json(); return new Response( JSON.stringify(db[routeParams.id]), { headers: { 'Content-Type': 'application/json' } } ); } } ``` ```ts interface User { id: string, name: string } interface Db { [id: string]: User; } interface RouteParams { id: string; } const db: Db = { '13': { id: '13', name: 'Dan' } } function isUser(obj: unknown): obj is User { return typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj; } export default async function CreateOrUpdateUser(req: Request, routeParams: RouteParams) { const maybeUser = await req.json(); if (!isUser(maybeUser)) { return new Response('Invalid user', { status: 400 }); } db[routeParams.id] = maybeUser; return new Response( JSON.stringify(db[routeParams.id]), { headers: { 'Content-Type': 'application/json' }} ); } ``` ## Middlewares Implementation For detailed information, please refer to the [Middlewares Section](/docs/middlewares). Generally, the interface can be described as follows: ```js export default function useMyMiddleware(req, next) { // Code before // ... await next(); // <-- The router's code or next middleware (on nested levels) // ... // Code after } ``` ```ts export default function useMyMiddleware(req: Request, next: NextFunction) { // Code before // ... await next(); // <-- The router's code or next middleware (on nested levels) // ... // Code after } ``` --- ## Route Matching ## Direct Matching A URL can accurately reflect the path to a specific file within your project's structure. For example, `/products/top-10` would refer to the file `/products/top-10.js`. When there is nesting in the path, but you want it to be invoked, use `index.js`. For instance, `/products/top-10` will map to either `/products/top-10.js` or `/products/top-10/index.js`. | Route | Example URL | |--------------------|------------------| | /index.js | / | | /products/`index.js` | /`products` | | /products/`top-10.js` | /products/`top-10` | ## Exact Matching To capture exactly one parameter, use the following syntax in the folder or file name: `[paramName]`. | Route | Example URL | Params | Notes | |------------------------------------------|--------------------------|------------------------------------|------------------------------------------------------------| | /products/`[id].js` | /products/`123` | ```{ id: '123' }``` | | | /categories/`[catId]`/products/`[prodId].js` | /categories/`1`/products/`2` | ```{ catId: '1', prodId: '2' }``` | Any nesting level | | /categories/`[id]`/products/`[id].js` | /categories/`1`/products/`2` | ```{ id: '2' }``` | If slug IDs have the same name, the last one will be taken. | | /products/`top-10.js` | /products/`top-10` | | You can still use direct matching. It will be prioritized over dynamic matching. | ## Catching All This type of matching can identify a list of slugs, but only if they are provided. | Route | Example URL | Params | Notes | |--------------------------------|--------------------------------|----------------------------------------------------|-----------------------------| | /categories/`[...categories]`.js | /categories/men/shoes/sneakers | ```{ categories: ['men', 'shoes', 'sneakers'] }``` | | | /categories/`[...categories]`.js | /categories/men | ```{ categories: ['men'] }``` | | | /categories/`[...categories]`.js | /categories | x | Won't capture anything, 404 | ## Optional Catching All This document is nearly identical to the previous one. The main difference is that it includes the root when no parameters are specified. | Route | Example URL | Params | Notes | |----------------------------------|--------------------------------|----------------------------------------------------|--------------| | /categories/`[[...categories]]`.js | /categories/`men/shoes/sneakers` | ```{ categories: ['men', 'shoes', 'sneakers'] }``` | | | /categories/`[[...categories]]`.js | /categories/`men` | ```{ categories: ['men'] }``` | | | /categories/`[[...categories]]`.js | /categories | | Will capture | --- ## Middlewares Middlewares are functions that execute before the main API handler. They are used for performing tasks such as authentication verification, request logging, error handling, and more. You can apply middleware to the entire application, to specific routes, or to a scope of routes within a folder. ## Usage For more examples, see the [examples](https://github.com/Danilqa/node-file-router/tree/main/examples/typescript) folder. ### File Location and Scopes To implement middleware, create a file named `middleware.js` (or .ts, .cjs, etc.) and place it in the api folder. This file will be executed for every request. ``` ├── api/ │ └── middleware.js <- ├── server.js └── package.json ``` To limit the scope, place the middleware file inside a specific folder. For example: ``` ├── api/ │ ├── users/ │ │ ├── ... │ │ └── middleware.js <- │ └── index.js ... ``` In this case, the middleware will only execute for endpoints under `/users/*`. ### Middleware Declaration Each middleware receives all the arguments passed from the initial request handler, with the last argument always being the next function. This is provided automatically, giving you control over the incoming arguments. Depending on the platform, you can utilize `req` and `res` objects, just the `req` object, or any other arguments you need. Here's an example of a middleware implementation in a Node.js/Express.js API: ```js export default function useMyMiddleware(req, res, next) { // Code before // ... await next(); // <-- The router's code or next middleware (on nested levels) // ... // Code after } ``` ```ts export default function useMyMiddleware( req: IncomingMessage, res: ServerResponse, next: NextFunction ) { // Code before // ... await next(); // <-- The router's code or next middleware (on nested levels) // ... // Code after } ``` ### List of Middlewares To compose multiple middlewares, arrange them in an array and export it as the default. They will execute sequentially in the order they are listed. ```js // file: api/middlewares.js export default [ useErrorHandler, useLogger, useAuthGuard ]; ``` ## Usage in File Routes To use middlewares in file routes, just return an array with middlewares before the request handler. For instance: ```js // file: api/users/[id].[post].js export default [ useSchemaValidation(PersonSchema), createUser ]; function createUser(req, res, routeParams) { // ... } ``` ```js // file: api/users/[id].[post].js export default [ useSchemaValidation(PersonSchema), createUser ]; function createUser(req, routeParams) { // ... } ``` In this example setup, `useSchemaValidation(PersonSchema)` serves as a middleware that validates the request against `PersonSchema`. Once the request passes through this middleware, it then proceeds to the createUser function, which handles the actual request logic. In the context of a route handler, route parameters become accessible to middleware functions after the `next` argument. For example: ```js // file: api/users/[id].[post].js export async function useValidation(req, res, next, routeParams) { // ... } export default [ useValidation, // ... ]; ``` ```js // file: api/users/[id].[post].js export async function useValidation(req, next, routeParams) { // ... } export default [ useValidation, // ... ]; ``` ## Interrupting the Chain To interrupt the chain of middlewares, call a return before `await next()` or throw an error. If you're interested in how to handle such errors inside other middlewares, see the [Error Handling](#error-handling) section. This is an example code of a middleware that checks if the user is authenticated. All middlewares after this one including the matched request handler, will not be executed. Here's an example of an authentication middleware, demonstrating how to interrupt the middleware chain: ```js export async function useAuth(req, res, next) { const auth = req.headers.authorization; // Some logic to check if the user is authenticated if (!auth) { return res .writeHead(401, { 'Content-Type': 'text/html' }) .end('Not Authorized'); } await next(); } ``` ```js export async function useAuth(req, res, next) { const auth = req.headers.authorization; // Some logic to check if the user is authenticated if (!auth) { return res.status(401).send('Unauthorized'); } await next(); } ``` ```js export async function useAuth(req, next) { const auth = req.headers.get('Authorization'); // Some logic to check if the user is authenticated if (!auth) { return new Response('Unauthorized', { status: 401 }); } await next(); } ``` The diagram illustrating the described process: ## Error Handling As mentioned previously, you can interrupt the middleware chain by throwing an error. Occasionally, other middlewares or the request handler itself might accidentally encounter an error. To manage these errors, simply wrap `await next()` in a try/catch block within the middleware. Place this logic early in the middleware chain to ensure it captures errors from all subsequent middlewares and request handlers. ```js export async function useErrorHandler(req, res, next) { try { await next(); } catch (error) { return res .writeHead(500, { 'Content-Type': 'text/html' }) .end(error.message); } } ``` ```js export async function useErrorHandler(req, res, next) { try { await next(); } catch (error) { res.status(500).send(error.message); } } ``` ```js export async function useErrorHandler(req, next) { try { await next(); } catch (error) { return new Response(error.message, { status: 500 }); } } ``` ## Shared State (Request Context) You can establish a shared state between middlewares and request handlers. To achieve this, pass your state object (or multiple ones) to the main request handler. For instance: ```js // ... const server = http.createServer((req, res) => { const ctx = {}; useFileRouter(req, res, ctx); }); // ... ``` This state can then be accessed in any middleware: ```js export default async function useMyMiddleware(req, res, ctx, next) { // ... ctx.myState = 'some value'; await next(); // ... } ``` Similarly, it can be accessed in the request handler: ```js export default function createUser(req, res, ctx) { // ... console.log(ctx.myState); // 'some value' // ... } ``` The `node-file-router` is designed for flexibility in its interface. You can introduce any number of arguments at the beginning, and these arguments will remain accessible throughout all stages of the processing chain. ## Returning Result In some situations, you may need to use an object as the final outcome of middleware, or modify one arrived from a request handler. A good example is altering a `Response` object in `Bun.js`. To do this, just return a value from the middleware. This value will then be the result of the `await next()` call. ```js export async function useCors(req, next) { const res = await next(); if (!res) return; res.headers.set('Access-Control-Allow-Methods', 'PUT'); return res; } ``` ```ts export async function useCors(req: Request, next: NextFunction) { const res = await next(); if (!res) return; res.headers.set('Access-Control-Allow-Methods', 'PUT'); return res; } ``` :::warning Note Remember to return the result of `await next()` in the subsequent middlewares if you wish to utilize the outcome from later stages. In other scenarios, returning a result from `next` is not mandatory. ::: The value from middleware is returned from the `useFileRouter` function, so you can use it in the same way as you would use the result from the request handler. ```js const useFileRouter = await initFileRouter(); // ... const response = await useFileRouter(req); ``` --- ## Custom Adapter If you want to use protocols other than HTTP or different frameworks, you can create your own adapter. To do this, you will need to pass the following properties to the `adapter` property: | Param | Description | HTTP analogy | |-----------|-------------| ------------- | | getPathname | The path that will be parsed and matched with the file. | `/products/123/options/78` | | getMethod | Refers to a specific action. This is used in [object type files.](/docs/usage-guide#object-with-methods) | `GET`, `POST`, `PATCH` | | defaultNotFoundHandler | This callback will be invoked when no file is found. | | Example for WebSockets. ```javascript {5-9} showLineNumbers const useFileRouter = await initFileRouter({ baseDir: `${__dirname}/api`, adapter: { getPathname: incomeMessage => incomeMessage.path, getMethod: incomeMessage => incomeMessage.action, defaultNotFoundHandler: (incomeMessage, ws) => ws.send(`${incomeMessage.path} is not found.`) } }); ``` ```typescript {6-10} showLineNumbers const useFileRouter = await initFileRouter({ baseDir: `${__dirname}/api`, adapter: { getPathname: incomeMessage => incomeMessage.path, getMethod: incomeMessage => incomeMessage.action, defaultNotFoundHandler: (incomeMessage, ws) => ws.send(`${incomeMessage.path} is not found.`) } }); ``` To get a working sample, check out [/examples/web-socket](https://github.com/Danilqa/node-file-router/tree/main/examples/web-socket/src) in the repository.