# directus-extension-api-docs

[![npm version](https://img.shields.io/npm/v/directus-extension-api-docs.svg)](https://www.npmjs.com/package/directus-extension-api-docs)
[![npm downloads](https://img.shields.io/npm/dm/directus-extension-api-docs.svg)](https://www.npmjs.com/package/directus-extension-api-docs)
[![license](https://img.shields.io/npm/l/directus-extension-api-docs.svg)](./LICENSE)

> Compatible with Directus `^9 || ^10 || ^11` and both bundled and non-bundled endpoint extensions.

Release notes: see [CHANGELOG.md](./CHANGELOG.md).

Directus Extension providing:

-   a Swagger UI interface (OpenAPI 3.x)
-   an autogenerated OpenAPI specification file (merged core + your custom endpoints)
    -- including custom endpoint definitions
-   optional validation middleware for your custom endpoints (based on merged OpenAPI spec). See details below
-   optional Zod-first route definitions for type-safe validation + OpenAPI auto-generation (see [Zod-first routes](#zod-first-routes-optional))

![workspace](assets/swagger.png)

## Contents

-   [Prerequisites](#prerequisites)
-   [Installation](#installation)
-   [Configuration (optional)](#configuration-optional)
-   [Definitions (optional)](#definitions-optional) — YAML
-   [Validations (optional)](#validations-optional) — YAML-based runtime validation
-   [Zod-first routes (optional)](#zod-first-routes-optional) — type-safe alternative
-   [YAML vs Zod](#yaml-vs-zod)
-   [Contributing](#contributing) · [Reporting issues](#reporting-issues) · [License](#license)

## Prerequisites

You must already have a Directus Node.js project running.

Ref: https://github.com/directus/directus

## Installation

```
npm install directus-extension-api-docs
```

-   Swagger interface: by default `http://localhost:8055/api-docs`
-   Openapi documentation: by default `http://localhost:8055/api-docs/oas`

## Configuration (optional)

To include your custom endpoints in the documentation, create an `oasconfig.yaml` file directly under the `/extensions` folder (recommended structure). Avoid placing it under `/extensions/endpoints` unless using Legacy mode.

Options:

-   `docsPath` _optional_ interface base path (default 'api-docs'). Resulting URLs: `/<docsPath>` and `/<docsPath>/oas`.
-   `info` _optional_ openapi server info (default extract from package.json)
-   `tags` _optional_ openapi custom tags (will be merged with all standard and all customs tags)
-   `publishedTags` _optional_ if specified, only operations containing at least one of these tags are kept; all other paths and unused tags are removed.
-   `paths` _optional_ custom path objects keyed by full path (e.g. `/my-custom-path/my-endpoint`). These are merged into Directus core paths.
-   `components` _optional_ custom components (schemas, securitySchemes, etc.) shallow-merged over core components.
-   `useAuthentication` _optional_ (default false). When true, `/api-docs` and `/api-docs/oas` stay publicly reachable: without valid auth they list only anonymous/public paths (no custom endpoints); with auth they list only paths permitted to that user under Directus Access Policies and custom endpoints.

Example below:

```
docsPath: 'api-docs'
useAuthentication: true
info:
  title: my-directus-bo
  version: 1.5.0
  description: my server description
tags:
- name: MyCustomTag
  description: MyCustomTag description
publishedTags:
- MyCustomTag
components:
  schemas:
    UserId:
      type: object
      required:
      - user_id
      x-collection: directus_users
      properties:
        user_id:
          description: Unique identifier for the user.
          example: 63716273-0f29-4648-8a2a-2af2948f6f78
          type: string

```

## Definitions (optional)

For each endpoint extension, you can define OpenAPI partials by adding an `oas.yaml` file in the root of that endpoint's folder.

### Non-bundled extensions

Place the `oas.yaml` file directly in the extension folder:

```
- ./extensions/
  ─ oasconfig.yaml (optional)
  - my-endpoint-extension/
    - oas.yaml
```

### Bundled extensions

For bundled extensions, place `oas.yaml` files in each sub-extension's folder under the `src` directory:

```
- ./extensions/
  ─ oasconfig.yaml (optional)
  - my-bundle-extension/
    - src/
      - routes-endpoint/
        - oas.yaml
      - admin-endpoint/
        - oas.yaml
```

This structure follows Directus's standard bundle architecture where each sub-extension (routes, endpoints, hooks, etc.) has its own folder under `src/`. The extension will automatically discover and merge all `oas.yaml` files from these subdirectories.

### Mixed environments

Both bundled and non-bundled extensions can coexist in the same project. The extension will automatically detect and merge all `oas.yaml` files from both types.

Properties:

-   `tags` _optional_ openapi custom tags
-   `paths` _optional_ openapi custom paths
-   `components` _optional_ openapi custom components

Example below (`./extensions/my-endpoint-extensions/oas.yaml`):

```
tags:
- name: MyCustomTag2
  description: MyCustomTag description2
paths:
  "/my-custom-path/my-endpoint":
    post:
      security:
        - Auth: [ ]
      summary: Validate email
      description: Validate email
      tags:
        - MyCustomTag2
        - MyCustomTag
      requestBody:
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/UserId"
      responses:
        '200':
          description: Successful request
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Users"
        '401':
          description: Unauthorized
          content: {}
        '422':
          description: Unprocessable Entity
          content: {}
        '500':
          description: Server Error
          content: {}
components:
  schemas:
    Users:
      type: object # ref to standard components declaring it empty
  securitySchemes:
    Auth:
      in: header
      name: Authorization
      type: apiKey
```

### Legacy mode

Configuration and definitions can also be managed in this legacy structure (still supported, but prefer the simplified root placement):

```
- ./extensions/
  - endpoints/
    - oasconfig.yaml
    - my-endpoint-extensions/
      - oas.yaml
    - my-endpoint-extensions2/
      - oas.yaml
```

## Validations (optional)

You can enable a request validation middleware based on your merged custom definitions.

Call the `validate` function inside your custom endpoint source (`./extensions/my-endpoint-extensions/src/index.js`).

Arguments: `validate(router, services, schema, paths?)`. `paths` (optional array) lets you restrict validation to only specific path keys from `oasconfig.yaml` instead of all custom paths.

Example below:

```
const { validate } = require('directus-extension-api-docs')

export default {
    id: 'my-custom-path',
    handler: async (router, { services, getSchema }) => {
        const schema = await getSchema();
        await validate(router, services, schema); // Enable validator

        router.post('/my-endpoint', async (req, res, next) => {
            ...
        })
    },
}
```

## Zod-first routes (optional)

An alternative ergonomic API: declare schemas and handlers together. The OpenAPI fragment is generated and merged into the same spec served at `/api-docs/oas`, and request validation runs automatically before each handler. Everything you need ships from this package — no need to import `defineEndpoint` from the Directus SDK.

```ts
const { defineEndpoint, z, registerSchema } = require('directus-extension-api-docs');

const UserId = registerSchema(
    'UserId',
    z.object({
        user_id: z.string().uuid().openapi({ example: '63716273-0f29-4648-8a2a-2af2948f6f78' }),
    }),
);

module.exports = defineEndpoint('my-custom-path', (route, { services, getSchema }) => {
    route({
        method: 'post',
        path: '/my-endpoint',
        tags: ['MyCustomTag'],
        summary: 'Validate user id',
        security: [{ Auth: [] }],
        request: { body: UserId },
        responses: {
            200: { description: 'OK', schema: UserId },
            401: { description: 'Unauthorized' },
        },
        handler: async (req, res) => {
            // req.body is typed: { user_id: string }
            res.json({ user_id: req.body.user_id });
        },
    });
});
```

Notes:
- The OpenAPI prefix defaults to `/<id>` so paths in `/api-docs/oas` match the URLs clients actually call (Directus mounts each endpoint extension under `/<id>`).
- `services`, `getSchema`, `logger`, ... are available in the setup closure and naturally accessible from each handler.
- For finer control (e.g. mixing Zod and raw Express routes), the lower-level `defineRoute(router, config)` is still exported.

Public exports (named exports of the package main, alongside `validate`):

| Export            | Purpose                                                                          |
| ----------------- | -------------------------------------------------------------------------------- |
| `defineEndpoint`  | Declarative wrapper for a Directus endpoint extension built on Zod routes.       |
| `defineRoute`     | Lower-level route registration when you already have your own router.            |
| `registerSchema`  | Register a reusable Zod schema as `components.schemas.<name>` (emits `$ref`).    |
| `z`               | Re-exported `zod` already extended with `.openapi()` metadata.                   |
| `zodValidator`    | The per-slot validation middleware used internally, for advanced use.            |

Validation errors are returned as HTTP `400` with the same `{ message, errors[] }` envelope used by `express-openapi-validator`, so existing API consumers don't need to change. Coexists with YAML definitions: pick whichever fits each endpoint.

## YAML vs Zod

Both pipelines feed the same `/api-docs/oas` document and can be mixed per endpoint.

-   **YAML** (`oasconfig.yaml` + `oas.yaml`): preferred when the OpenAPI spec is the source of truth, or when you import an existing spec. Runtime validation is opt-in via `validate(...)`.
-   **Zod**: preferred when you want one source of truth that also types `req.body` / `req.params` / `req.query` and validates by default. No separate YAML file to keep in sync.

## Contributing

Issues and pull requests are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, scripts, and commit conventions.

## Reporting issues

Bugs and feature requests: [github.com/sacconazzo/directus-extension-api-docs/issues](https://github.com/sacconazzo/directus-extension-api-docs/issues).

For security vulnerabilities, please follow [SECURITY.md](./SECURITY.md) instead of opening a public issue.

## License

[MIT](./LICENSE) © Giona Righini (sacconazzo)
