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