# @fastify/autoload

[![CI](https://github.com/fastify/fastify-autoload/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-autoload/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/@fastify/autoload.svg?style=flat)](https://www.npmjs.com/package/@fastify/autoload)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)

Convenience plugin for Fastify that loads all plugins found in a directory and automatically configures routes matching the folder structure.

## Installation

```
npm i @fastify/autoload
```

### Compatibility
| Plugin version | Fastify version |
| ---------------|-----------------|
| `>=6.x`        | `^5.x`          |
| `^5.x`         | `^4.x`          |
| `>=2.x <5.x`   | `^3.x`          |
| `^1.x`         | `^2.x`          |
| `^1.x`         | `^1.x`          |


Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin
in the table above.
See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details.

## Example

Fastify server that automatically loads in all plugins from the `plugins` directory:

```js
const fastify = require('fastify')
const autoload = require('@fastify/autoload')

const app = fastify()

app.register(autoload, {
  dir: path.join(__dirname, 'plugins')
})

app.listen({ port: 3000 })
```

or with ESM syntax:

```js
import autoLoad from '@fastify/autoload'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import fastify from 'fastify'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const app = fastify()

app.register(autoLoad, {
  dir: join(__dirname, 'plugins')
})

app.listen({ port: 3000 })
```

Folder structure:

```
├── plugins
│   ├── hooked-plugin
│   │   ├── autohooks.mjs
│   │   ├── routes.js
│   │   └── children
│   │       ├── commonjs.cjs
│   │       ├── module.mjs
│   │       └── typescript.ts
│   ├── single-plugin
│   │   ├── index.js
│   │   └── utils.js
│   ├── more-plugins
│   │   ├── commonjs.cjs
│   │   ├── module.mjs
│   │   └── typescript.ts
│   └── another-plugin.js
├── package.json
└── app.js
```

## Global Configuration

Autoload can be customized using the following options:

- `dir` (required) - Base directory containing plugins to be loaded

  Each script file within a directory is treated as a plugin unless the directory contains an index file (e.g. `index.js`). In which case, only the index file (and the potential sub-directories) will be loaded.

  The following script types are supported:

  - `.js ` (CommonJS or ES modules depending on `type` field of parent `package.json`)
  - `.cjs` (CommonJS)
  - `.mjs` (ES modules)
  - `.ts` (TypeScript)

- `dirNameRoutePrefix` (optional) - Default: true. Determines whether routes will be automatically prefixed with the subdirectory name in an autoloaded directory. It can be a sync function that must return a string that will be used as prefix, or it must return `false` to skip the prefix for the directory.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'routes'),
    dirNameRoutePrefix: false // lack of prefix will mean no prefix, instead of directory name
  })

  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'routes'),
    dirNameRoutePrefix: function rewrite (folderParent, folderName) {
      if (folderName === 'YELLOW') {
        return 'yellow-submarine'
      }
      if (folderName === 'FoOoO-BaAaR') {
        return false
      }
      return folderName
    }
  })
  ```

- `matchFilter` (optional) - Filter matching any path that should be loaded. Can be a RegExp, a string, or a function returning a boolean.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    matchFilter: (path) => path.split("/").at(-2) === "handlers"
  })
  ```


- `ignoreFilter` (optional) - Filter matching any path that should not be loaded. Can be a RegExp, a string ,or a function returning a boolean.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    ignoreFilter: (path) => path.endsWith('.spec.js')
  })
  ```


- `ignorePattern` (optional) - RegExp matching any file or folder that should not be loaded.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    ignorePattern: /^.*(?:test|spec).js$/
  })
  ```


- `scriptPattern` (optional) - Regex to override the script files accepted by default. You should only use this option
with a [customization hooks](https://nodejs.org/docs/latest/api/module.html#customization-hooks)
provider, such as `ts-node`. Otherwise, widening the acceptance extension here will result in an error.


  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    scriptPattern: /(?<!\.d)\.(ts|tsx)$/
  })
  ```

- `indexPattern` (optional) - Regex to override the `index.js` naming convention

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    indexPattern: /^.*routes(?:\.ts|\.js|\.cjs|\.mjs)$/
  })
  ```

- `maxDepth` (optional) - Limits the depth at which nested plugins are loaded

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    maxDepth: 2 // files in `opts.dir` nested more than 2 directories deep will be ignored.
  })
  ```

- `forceESM` (optional) - If set to 'true' it always use `await import` to load plugins or hooks.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    forceESM: true
  })
  ```
- `encapsulate` (optional) - Defaults to 'true', if set to 'false' each plugin loaded is wrapped with [fastify-plugin](https://github.com/fastify/fastify-plugin). This allows you to share contexts between plugins and the parent context if needed. For example, if you need to share decorators. Read [this](https://github.com/fastify/fastify/blob/main/docs/Reference/Encapsulation.md#sharing-between-contexts) for more details.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    encapsulate: false
  })
  ```

- `options` (optional) - Global options object used for all registered plugins

  Any option specified here will override `plugin.autoConfig` options specified in the plugin itself.

  When setting both `options.prefix` and `plugin.autoPrefix` they will be concatenated.

  ```js
  // index.js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    options: { prefix: '/defaultPrefix' }
  })

  // /plugins/something.js
  module.exports = function (fastify, opts, next) {
    // your plugin
  }

  module.exports.autoPrefix = '/something'

  // /plugins/something.mjs
  export default function (f, opts, next) {
    f.get('/', (request, reply) => {
      reply.send({ something: 'else' })
    })

    next()
  }

  export const autoPrefix = '/prefixed'

  // routes can now be added to /defaultPrefix/something
  ```

- `autoHooks` (optional) - Apply hooks from `autohooks.js` file(s) to plugins found in folder

  Automatic hooks from `autohooks` files will be encapsulated with plugins. If `false`, all `autohooks.js` files will be ignored.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    autoHooks: true // apply hooks to routes in this level
  })
  ```

  If `autoHooks` is set, all plugins in the folder will be [encapsulated](https://github.com/fastify/fastify/blob/main/docs/Reference/Encapsulation.md)
  and decorated values _will not be exported_ outside the folder.

- `autoHooksPattern` (optional) - Regex to override the `autohooks` naming convention

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    autoHooks: true,
    autoHooksPattern: /^[_.]?auto_?hooks(?:\.js|\.cjs|\.mjs)$/i
  })
  ```

- `cascadeHooks` (optional) - If using `autoHooks`, cascade hooks to all children. Ignored if `autoHooks` is `false`.

  Default behavior of `autoHooks` is to apply hooks only to the level on which the `autohooks.js` file is found. Setting `cascadeHooks: true` will continue applying the hooks to any children.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    autoHooks: true, // apply hooks to routes in this level,
    cascadeHooks: true // continue applying hooks to children, starting at this level
  })
  ```

- `overwriteHooks` (optional) - If using `cascadeHooks`, cascade will be reset when a new `autohooks.js` file is encountered. Ignored if `autoHooks` is `false`.

  Default behavior of `cascadeHooks` is to accumulate hooks as new `autohooks.js` files are discovered and cascade to children. Setting `overwriteHooks: true` will start a new hook cascade when new `autohooks.js` files are encountered.

  ```js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    autoHooks: true, // apply hooks to routes in this level,
    cascadeHooks: true, // continue applying hooks to children, starting at this level,
    overwriteHooks: true // re-start hook cascade when a new `autohooks.js` file is found
  })
  ```

- `routeParams` (optional) - Folders prefixed with `_` will be turned into route parameters.

  If you want to use mixed route parameters use a double underscore `__`.

  ```js
  /*
  ├── routes
  ├── __country-__language
  │   │  └── actions.js
  │   └── users
  │       ├── _id
  │       │   └── actions.js
  │       ├── __country-__language
  │       │   └── actions.js
  │       └── index.js
  └── app.js
  */

  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'routes'),
    routeParams: true
    // routes/users/_id/actions.js will be loaded with prefix /users/:id
    // routes/__country-__language/actions.js will be loaded with prefix /:country-:language
  })

  // curl http://localhost:3000/users/index
  // { userIndex: [ { id: 7, username: 'example' } ] }

  // curl http://localhost:3000/users/7/details
  // { user: { id: 7, username: 'example' } }

  // curl http://localhost:3000/be-nl
  // { country: 'be', language: 'nl' }
  ```

## Override TypeScript detection using an environment variable
This plugin uses [native type stripping](https://nodejs.org/docs/latest-v23.x/api/typescript.html#modules-typescript) with Node 23 and later.

It is possible to override the automatic detection of a TypeScript-capable runtime using the `FASTIFY_AUTOLOAD_TYPESCRIPT` environment variable. If set to a truthy value Autoload will load `.ts` files, expecting that node has a TypeScript-capable loader.

This is useful for cases where you want to use Autoload for loading TypeScript files but detecting the TypeScript loader fails because, for example, you are using a custom loader.

It can be used like this:

```sh
FASTIFY_AUTOLOAD_TYPESCRIPT=1 node --loader=my-custom-loader index.ts
```

## Plugin Configuration

Each plugin can be individually configured using the following module properties:

- `plugin.autoConfig` - Specifies the options to be used as the `opts` parameter.

  ```js
  module.exports = function (fastify, opts, next) {
    console.log(opts.foo) // 'bar'
    next()
  }

  module.exports.autoConfig = { foo: 'bar' }
  ```

  Or with ESM syntax:

  ```js
  import plugin from '../lib-plugin.js'

  export default async function myPlugin (app, options) {
    app.get('/', async (request, reply) => {
      return { hello: options.name }
    })
  }
  export const autoConfig = { name: 'y' }
  ```

  You can also use a callback function if you need to access the parent instance:
  ```js
  export const autoConfig = (fastify) => {
    return { name: 'y ' + fastify.rootName }
  }
  ```

  However, note that the `prefix` option should be set directly on `autoConfig` for autoloading to work as expected:
  ```js
  export const autoConfig = (fastify) => {
    return { name: 'y ' + fastify.rootName }
  }

  autoConfig.prefix = '/hello'
  ```

- `plugin.autoPrefix` - Set routing prefix for plugin

  ```js
  module.exports = function (fastify, opts, next) {
    fastify.get('/', (request, reply) => {
      reply.send({ hello: 'world' })
    })

    next()
  }

  module.exports.autoPrefix = '/something'

  // when loaded with autoload, this will be exposed as /something
  ```

  Or with ESM syntax:

  ```js
  export default async function (app, opts) {
    app.get('/', (request, reply) => {
      return { something: 'else' }
    })
  }

  export const autoPrefix = '/prefixed'
  ```


- `plugin.prefixOverride` - Override all other prefix options

  ```js
  // index.js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    options: { prefix: '/defaultPrefix' }
  })

  // /foo/something.js
  module.exports = function (fastify, opts, next) {
    // your plugin
  }

  module.exports.prefixOverride = '/overriddenPrefix'

  // this will be exposed as /overriddenPrefix
  ```

  Or with ESM syntax:

  ```js
  export default async function (app, opts) {
    // your plugin
  }

  export const prefixOverride = '/overriddenPrefix'
  ```

  If you have a plugin in the folder you do not want any prefix applied to, you can set `prefixOverride = ''`:

  ```js
  // index.js
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    options: { prefix: '/defaultPrefix' }
  })

  // /foo/something.js
  module.exports = function (fastify, opts, next) {
    // your plugin
  }

  // optional
  module.exports.prefixOverride = ''

  // routes can now be added without a prefix
  ```

- `plugin.autoload` - Toggle whether the plugin should be loaded

  Example:

  ```js
  module.exports = function (fastify, opts, next) {
    // your plugin
  }

  // optional
  module.exports.autoload = false
  ```

- `opts.name` - Set name of plugin so that it can be referenced as a dependency

- `opts.dependencies` - Set plugin dependencies to ensure correct load order

  Example:

  ```js
  // plugins/plugin-a.js
  const fp = require('fastify-plugin')

  function plugin (fastify, opts, next) {
    // plugin a
  }

  module.exports = fp(plugin, {
    name: 'plugin-a',
    dependencies: ['plugin-b']
  })

  // plugins/plugin-b.js
  function plugin (fastify, opts, next) {
    // plugin b
  }

  module.exports = fp(plugin, {
    name: 'plugin-b'
  })
  ```

  ## Autohooks:

  The autohooks functionality provides several options for automatically adding hooks, decorators, etc. to your routes. CJS and ESM `autohook` formats are supported.

  The default behavior of `autoHooks: true` is to encapsulate the `autohooks.js` plugin with the contents of the folder containing the file. The `cascadeHooks: true` option encapsulates the hooks with the current folder contents and all subsequent children, with any additional `autohooks.js` files being applied cumulatively. The `overwriteHooks: true` option will restart the cascade any time an `autohooks.js` file is encountered.

  Plugins and hooks are encapsulated together by folder and registered on the `fastify` instance that loaded the `@fastify/autoload` plugin. For more information on how encapsulation works in Fastify, see: https://fastify.dev/docs/latest/Reference/Encapsulation/#encapsulation

    ### Example:

    ```
    ├── plugins
    │   ├── hooked-plugin
    │   │   ├── autohooks.js // req.hookOne = 'yes' # CJS syntax
    │   │   ├── routes.js
    │   │   └── children
    │   │       ├── old-routes.js
    │   │       ├── new-routes.js
    │   │       └── grandchildren
    │   │           ├── autohooks.mjs // req.hookTwo = 'yes' # ESM syntax
    │   │           └── routes.mjs
    │   └── standard-plugin
    │       └── routes.js
    └── app.js
    ```

    ```js
    // hooked-plugin/autohooks.js

    module.exports = async function (app, opts) {
      app.addHook('onRequest', async (req, reply) => {
        req.hookOne = yes;
      });
    }

    // hooked-plugin/children/grandchildren/autohooks.mjs

    export default async function (app, opts) {
      app.addHook('onRequest', async (req, reply) => {
        req.hookTwo = yes
      })
    }
    ```

    ```bash
    # app.js { autoHooks: true }

    $ curl http://localhost:3000/standard-plugin/
    {} # no hooks in this folder, so behavior is unchanged

    $ curl http://localhost:3000/hooked-plugin/
    { hookOne: 'yes' }

    $ curl http://localhost:3000/hooked-plugin/children/old
    {}

    $ curl http://localhost:3000/hooked-plugin/children/new
    {}

    $ curl http://localhost:3000/hooked-plugin/children/grandchildren/
    { hookTwo: 'yes' }
    ```

    ```bash
    # app.js { autoHooks: true, cascadeHooks: true }

    $ curl http://localhost:3000/hooked-plugin/
    { hookOne: 'yes' }

    $ curl http://localhost:3000/hooked-plugin/children/old
    { hookOne: 'yes' }

    $ curl http://localhost:3000/hooked-plugin/children/new
    { hookOne: 'yes' }

    $ curl http://localhost:3000/hooked-plugin/children/grandchildren/
    { hookOne: 'yes', hookTwo: 'yes' } # hooks are accumulated and applied in ascending order
    ```

    ```bash
    # app.js { autoHooks: true, cascadeHooks: true, overwriteHooks: true }

    $ curl http://localhost:3000/hooked-plugin/
    { hookOne: 'yes' }

    $ curl http://localhost:3000/hooked-plugin/children/old
    { hookOne: 'yes' }

    $ curl http://localhost:3000/hooked-plugin/children/new
    { hookOne: 'yes' }

    $ curl http://localhost:3000/hooked-plugin/children/grandchildren/
    { hookTwo: 'yes' } # new autohooks.js takes over
    ```

## License

Licensed under [MIT](./LICENSE).
