# Adaptivestone Framework - Complete Documentation

> This file is auto-generated from the Docusaurus documentation.
> Generated on: 2026-07-05T14:50:34.412Z
> Total documents: 22

---

## Table of Contents

1. 01-intro
2. 02-configs
3. 03-files-inheritance
4. 04-base
5. 05-models
6. 05-modelsOld
7. 06-Controllers / 01-intro
8. 06-Controllers / 02-routes
9. 06-Controllers / 03-middleware
10. 06-Controllers / 04-error-handling
11. 07-logging
12. 08-i18n
13. 09-testsing
14. 10-cli
15. 11-cache
16. 12-email
17. 12-resize
18. 13-deploy
19. 14-helpers
20. 15-recipes
21. 16-anti-patterns
22. 17-openapi

---



# Document 1: 01-intro

<!-- Source: 01-intro.md -->

# Intro

Welcome to the AdaptiveStone framework documentation. We hope that the documentation is clean and short. Please feel free to edit it via a Git merge request and contact us.

## History

You must be wondering - why another framework, as we currently have a lot of them? But each of them is different.

The Adaptive Stone framework was born around 2016 and has been growing since.

There were a few requirements that no one existing framework could provide us for SAAS at that time - working with multiple databases, scaling out of the box, and a modern approach.

That is why the Adaptive Stone framework was born.

## Features

- Automatically initialized controllers (no config needed)
- Mongoose models
- Winston logger (Sentry logger as a part of Winston logger)
- Node cluster enabled by default (possibility to use with no cluster (dev mode))
- Docker development environment (and production too)
- Integrated Biome (modern alternative to ESLint and Prettier) for code style
- Cache system
- Ability to overwrite any controller, model, and config that came with the framework
- Multi-language support out of the box
- ESM only (no CommonJS)
- TypeScript support (you are able to write everything in JavaScript, but in JS you will have types as a bonus)

:::info Requirements
The framework requires **MongoDB** and an **`AUTH_SALT`** secret — it fails fast at boot if either is missing (set the `MONGO_DSN` env var; generate a salt with `npm run cli generateRandomBytes`). The runtime requires **Node ≥ 24**.
:::

## Folder Structure

```js
framework/
├─ commands/  // Contains the command folder (CLI)
├─ config/ // Contains config files
├─ controllers/ // Contains controller files
│  ├─ {controller_group_folder}/ // Can contain a folder (will be added to the route)
├─ locales/ // i18n folder with translations
│  ├─ {locale_1}/ // Locale name (en, fr, etc.)
│  │  ├─ translation.json // Translation JSON file
│  ├─ {locale_2}/
│  │  ├─ translation.json
├─ migrations/ // Folder where migration files are stored
├─ models/ // Contains model files
├─ modules/ // Main folder with abstract stuff
├─ public/ // Public stuff (served statically)
├─ services/ // Some services (email, http, etc.)
├─ tests/ // Folder contains basic tests
├─ Cli.ts // Main CLI class
├─ index.ts // Main entry point (creates and starts the Server)
├─ cluster.ts  // Entry point for production (cluster module)
├─ folderConfig.ts // Folder configuration
├─ server.ts // Server class
```

## Framework Structure

![Framework](/img/AdaptiveStroneFramework.jpg)

## Getting Started

Get started by **creating a new project**.

The simplest way to create a new project is to clone the **template** and customize it:

```shell
git clone git@github.com:adaptivestone/framework-example-project.git adaptivestone-example-rename-me
```

## Start Your Project

You should have **[Docker](https://www.docker.com/products/docker-desktop)** and **[Docker Compose](https://docs.docker.com/compose/install/)** installed.

Run the development server:

```shell
cd adaptivestone-example-rename-me
docker compose up
```

Your site starts at `http://localhost:3300`.

Open `src/controllers/Person.ts` and edit some lines: the site **reloads automatically** and applies your changes.

## TypeScript Support

The framework itself is written in TypeScript (erasable syntax), but the project is flexible to use either TypeScript or JavaScript.

You are able to use the modern Node.js runtime to run TypeScript files without any compilation.

You are also able to mix TypeScript and JavaScript files in one project. Just remember that types will only work if the imported file is a TypeScript file and it is imported into a TypeScript file.

## Guides

- [Recipes](15-recipes.md) — a task-oriented cookbook: add a controller, validate a body, paginate, write middleware, override a built-in, test a controller.
- [Anti-patterns](16-anti-patterns.md) — common mistakes and what to do instead.


---


# Document 2: 02-configs

<!-- Source: 02-configs.md -->

# Configs

Every modern application runs on multiple environments and should have the ability to configure itself without changing the code.

## Config Files

Config files are located in ‘src/config/\{filename\}.(js|ts)’. Each config file is mostly unique, and you should refer to the module documentation (or the framework config section).

:::note

Config files are part of the [framework inheritance process](03-files-inheritance.md).

:::

## Environment Variables

Out of the box, the framework supports environment variables with [process.loadEnvFile](https://nodejs.org/api/process.html#processloadenvfilepath). This is part of Node.js since v20.12.0.

The framework will grab and parse the .env file in the root of the project directory to fill environment variables.

The sample project ships with a basic .env.example file.

:::tip

Do not use environment variables in the code. Use them only inside config files. This will allow you to track variables and split configs and the codebase.

:::

:::tip

Do not push the .env file to the version control system, as it can contain passwords and keys. The file should be unique per environment.
:::

:::tip

Try not to overload the .env file with configuration and keep only private data in it. To manage non-private data, check the [NODE_ENV](#node_env) section.
:::

## NODE_ENV

Depending on the NODE_ENV environment variable, the framework will load additional config files. Data from these files will be merged into the main config with an overwrite.

This is useful when you want to have something that depends on the environment but do not want to overload the .env file.

Also, it's a good practice to keep the basic config in files rather than in the .env file.

**Example:**

```js title="/src/config/sample.ts"
export default {
  variable1: 1,
  variable2: 2,
};
```

```js title="/src/config/sample.production.ts"
export default {
  variable2: 3,
  variable3: 4,
};
```

On a NON-production environment:

```js
const sampleConfig = this.app.getConfig("sample");
//     variable1: 1,
//     variable2: 2
```

On a PRODUCTION environment:

```js
const sampleConfig = this.app.getConfig("sample");
//     variable1: 1,
//     variable2: 3, <-- DIFFERENT
//     variable3: 4  <-- NEW
```

You can use the same approach for different environments (dev, stage, testing, etc.).

## API

```ts
getConfig(configName: string): Record<string, unknown>;
updateConfig(configName: string, config: Record<string, unknown>): Record<string, unknown>;
```

Returns the config based on the config name. It also caches it in memory.

```js
const sampleConfig = this.app.getConfig("sample");
```

Updates the config based on the config name. Returns the updated config.

```js
const sampleConfig = this.app.updateConfig("sample", {
  variableToUpdate: 3,
  anotherVariable: 4,
});
```

## Typed config

The base `getConfig(name)` signature is `Record<string, unknown>`, but the
framework's [type generation](10-cli.md) (`node src/cli.ts generatetypes`) emits
a `genTypes.d.ts` that types each call against your **actual** config file — so
`this.app.getConfig("http").port` is precise, with no cast.

The type is derived from the config's runtime **shape**, not its values, so a
secret value is never written into the generated file. A key read straight from
the environment with **no default** is a special case: at generation time
`process.env.X` may be unset, so the framework reads it from the config
**source** and types it honestly as `string | undefined`.

```ts title="/src/config/hubspot.ts"
export default {
  apiKey: process.env.HUBSPOT_API_KEY, // typed: string | undefined
  region: process.env.HUBSPOT_REGION ?? "eu", // has a default → typed: string
  baseUrl: "https://api.hubspot.com", // literal → typed: string
};
```

```ts
const { apiKey } = this.app.getConfig("hubspot");
// apiKey is `string | undefined` — guard it; no `as` cast needed
if (!apiKey) {
  throw new Error("HUBSPOT_API_KEY is not set");
}
```

:::tip

If a value is guaranteed present (you assert it at boot), add a non-null
assertion in the config so the generated type is `string`:

```ts
apiKey: process.env.HUBSPOT_API_KEY!, // typed: string
```

:::

:::note

Recovery follows only inline `process.env.X` reads in a config's own
default-exported object. A key whose env read is spread in from another config
(`{ ...baseConfig }`) or hidden behind an indirection isn't followed across that
boundary. Regenerate after changing a config's shape; `generatetypes --check`
fails CI if the committed types are stale.

:::


---


# Document 3: 03-files-inheritance

<!-- Source: 03-files-inheritance.md -->

# File Inheritance

The framework provides a flexible way to overwrite some functionalities from the core. You can create a file with the same name in the same folder, and the framework will use this file instead of the built-in one.

Let’s go with an example. We have a User model built directly with the framework with some user-related stuff. We want to provide a fully different implementation of this model.

To do that, we will create a user model file at the project level in the model folder.

```js
project/
├─ node_modules/
│  ├─ @adaptivestone/
│  │  ├─ framework/
│  │  │  ├─ models/
│  │  │  │  ├─ User.js // Built-in model (published as compiled .js). Mark it as "User_original"
├─ src/
│  ├─ models/ // Contains model files
│  │  ├─ User.ts // File that will be used. Mark it as "User_project"

```

```js
const User = this.app.getModel("User"); // Will return the "User_project" model
```

This also happens at **all** levels of the code. If some code inside the framework asks for the “User” model, it will get the “User_project” model.

The same approach works for models, controllers, and configs.

## How To

### Extend a module with new functionality instead of completely overwriting it

That's easy. Just require the original file and extend it.
Please note: TypeScript types are fully optional. You are able to use plain JavaScript too.

```ts
import OriginalUserModel from "@adaptivestone/framework/models/User.js";
import type {
  GetModelTypeLiteFromSchema,
  ExtractProperty,
} from "@adaptivestone/framework/modules/BaseModel.js";

class User extends OriginalUserModel {
  static get modelStatics() {
    type UserModelLite = GetModelTypeLiteFromSchema<
      typeof User.modelSchema,
      ExtractProperty<typeof User, "schemaOptions">
    >;

    return {
      ...OriginalUserModel.modelStatics, // Grab the original model methods
      getPublic: function getPublic(this: InstanceType<UserModelLite>) {
        return {
          userName: this.name,
        };
      },
    };
  }
}
```

### Disable functions completely

To disable something (like a default controller), the best way is to overwrite it with an empty implementation.

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class Auth extends AbstractController {}

export default Auth;
```


---


# Document 4: 04-base

<!-- Source: 04-base.md -->

# Base Class

Each class extends the Base class to have some basic functions, mostly related to logging and the inheritance process.

The Base class implements on-demand logging instance loading. That means the logger will be united only when you first request it. This is an important part of loading speed optimization.

## API

```js
class A extends Base {
  async someFunction() {
    // Access to the logger instance (please follow the logger documentation for more details).
    this.logger;

    // Get files with inheritance.
    const files = await this.getFilesPathWithInheritance(
      `${import.meta.dirname}/../../migrations`,
      this.app.foldersConfig.migrations
    );
  }

  /**
   * Returns the logger group. Just to have all logs grouped logically.
   */
  static get loggerGroup() {
    return "Base_please_overwrite_";
  }

  /**
   * In case of logging, sometimes we might need to replace the name.
   */
  getConstructorName() {
    return this.constructor.name;
  }
}
```

```js
getFilesPathWithInheritance(internalFolder, externalFolder);
```

Will scan two folders and provide a path with inheritance. If the same file is present in both paths, the priority will be given to the external file.


---


# Document 5: 05-models

<!-- Source: 05-models.md -->

# Models

The framework is based on the [Mongoose](https://mongoosejs.com/) library and provides direct access to it.

The model handles database connections.

:::note

Model files are part of the [framework inheritance process](03-files-inheritance.md).

:::

The model uses a class with static methods and properties, providing auto-typing for each model. TypeScript helpers are also available to extract types from the model class.

## Lifecycle

The framework can do the following:

1. Load model files
2. Initialize model files

There are internal options for this (see the Commands section), which are mostly used in commands where you can skip model initialization or load the model without initializing it. This is useful in a few use cases, primarily for type generation.

Under normal conditions, the framework scans the `model` folder and loads all models using the inheritance process.

This is primarily to avoid model 'circular dependencies.'

### Duplicate framework copies

Every model extends `BaseModel` from `@adaptivestone/framework`. If **two different copies** of the framework end up installed (a duplicate/undeduped install), a model can extend `BaseModel` from one copy while the loader runs from the other. `instanceof BaseModel` compares prototype identity, so it is `false` across that copy boundary — and boot used to silently misroute such a model into the legacy (`AbstractModel`) branch, surfacing only later as a confusing downstream failure.

Boot now recognizes the model by its static shape (a `BaseModel` subclass without the `instanceof`) and **fails fast**, naming the offending model:

```text
Model 'Article' extends BaseModel from a DIFFERENT copy of @adaptivestone/framework than the one loading models, so `instanceof BaseModel` is false and it cannot be initialized. This means @adaptivestone/framework is installed more than once (a duplicate/undeduped install). Fix the duplication so a single copy is shared: run `npm ls @adaptivestone/framework` to find the extra copy, then dedupe (align versions, delete node_modules and reinstall, and check your lockfile).
```

The model loader prefixes this with the model name and file (`Failed to initialize model '<Name>' (<file>): …`). The fix is to collapse the duplicate so a single copy is shared:

```bash
npm ls @adaptivestone/framework
```

then dedupe — align versions, delete `node_modules` and reinstall, and check your lockfile. Genuinely legacy (`AbstractModel`-based) models still route to the legacy branch unchanged.

:::tip Module authors

If you publish a package that defines framework models (or otherwise imports `@adaptivestone/framework`), declare the framework as a **peer dependency**, not a regular dependency — that way the host app supplies the single shared copy instead of your package pulling in a second one. `npm link` during local development is the most common trigger for a duplicate copy, since the linked package resolves the framework from its own `node_modules`.

:::

## Base Model

The base model is the core of your models. It handles their structure and initialization.
We have provided a TypeScript example, but you can ignore the types if you only want to use JavaScript.
Using types is fully optional.

```ts
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";

// in case you need to access appInstance - appInstance.getConfig('s3');
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";

// These are TypeScript helpers.
import type {
  GetModelTypeFromClass, // `GetModelTypeFromClass` returns model types from the class.
  GetModelTypeLiteFromSchema, // Same as above, but only uses the schema to avoid circular linking.
} from "@adaptivestone/framework/modules/BaseModel.js";

import mongoose, { type Schema } from "mongoose";

// Type helper for static and instance methods.
type SomeModelLite = GetModelTypeLiteFromSchema<typeof SomeModel.modelSchema>;

class SomeModel extends BaseModel {
  static initHooks(schema: Schema): void {
    // A place to initialize plugins, indexes, and so on.
    // This happens after the class is loaded into Mongoose but before Mongoose initializes it.
    // schema.plugin(PLUGIN_NAME);
    schema.index({ name: "text" }); // or indexes.

    // For hooks, there are two types of `this`: model and queries.
    // https://mongoosejs.com/docs/middleware.html#types-of-middleware
    schema.pre(
      'save',
      async function (this: InstanceType<SomeModelLite>) {
        ...
      }
    );
    schema.pre('findOneAndDelete', async function () {
      const docToDelete = await this.model.findOne<SomeModelLite>( // Helps to return the correct model.
        this.getQuery(),
      );
    });
  }

  // The Mongoose schema goes here.
  // This is a complete Mongoose schema.
  // Please refer to the Mongoose documentation: https://mongoosejs.com/docs/guide.html
  static get modelSchema() {
    return {
      someString: { type: String, required: true },
      firstName: String,
      lastName: String,
      email: String,
      orders: {
        type: mongoose.Schema.Types.ObjectId, // This is the correct type for an ObjectID reference. Only this type generates valid types.
        ref: "Order", // You don't need to worry about initialized model schemas; the framework will load and initialize all models for you.
      },
    } as const; // This helps generate better types (TypeScript only).
  }

  // The Mongoose schema options go here.
  // Please refer to the Mongoose documentation: https://mongoosejs.com/docs/guide.html#options
  static get schemaOptions() {
    return {
      read: "primary",
    } as const; // This helps to generate better types. (TypeScript only)
  }

  /**
   *  Object with static methods.
   * this.app.getModel('SomeModel').findByEmail('email');
   * this.app.getModel('SomeModel').getInfoStatic();
   *
   */
  static get modelStatics() {
    type OrderModelType = GetModelTypeFromClass<typeof Order>; // To help with the `populate` method.

    return {
      findByEmail: async function findByEmail(
        this: SomeModelLite, // A type helper to map to the correct `this` context.
        email: string
      ) {
        const instance = await this.find({ email });
        return instance;
      },
      getInfoStatic: async function getInfoStatic(
        model: InstanceType<SomeModelLite> // A TypeScript type helper.
      ) {
        await model.populate("orders");
        return {
          _id: model.id,
          email: model.email,
        };
      },
      getInfoStaticWithOrders: async function getInfoStatic(
        // Intercepts model types to ensure that the `orders` type is correct (without interception, it will be just an `ObjectID`).
        model: InstanceType<SomeModelLite> & {
          orders: InstanceType<OrderModelType>[];
        }
      ) {
        await model.populate("orders");
        return {
          _id: model.id,
          email: model.email,
          orders: model.orders,
        };
      },
    };
  }

  /**
   * We should also have instance methods for the model to interact with.
   * const SomeModel = appInstance.getModel('SomeModel');
   * const someModel = await SomeModel.findOne({email:"cfff"});
   * const data = await someModel.getInfo(); // call instance method
   */
  static get modelInstanceMethods() {
    type ShippingInstanceType = InstanceType<SomeModelLite>;

    return {
      getInfo: async function getInfo(this: SomeModelLite) {
        return {
          _id: this._id,
          email: this.email,
        };
      },
            // anotherMethod,
    };
  }

  /**
   * We should also have virtual methods for the model to interact with.
   * const SomeModel = appInstance.getModel('SomeModel');
   * const someModel = await SomeModel.findOne({email:"cfff"});
   * const fullName = await someModel.fullName // virtual field
   * someModel.fullName = 'Jean-Luc Picard';
   */
  static get modelVirtuals() {
    return {
      fullName: {
        // virtual field
        options: {
          type: Object, // schema
        },
        get(this: InstanceType<SomeModelLite>) {
          // Getter
          return `${this.firstName} ${this.lastName}`;
        },
        async set(this: InstanceType<SomeModelLite>, v: string) {
          // Setter
          const firstName = v.substring(0, v.indexOf(" "));
          const lastName = v.substring(v.indexOf(" ") + 1);
          this.set({ firstName, lastName });
        },
      },
    }; // make sure that you not put it as a const
  }

}

export default SomeModel;

// It's good practice to return the type from the model.
export type TSomeModel = GetModelTypeFromClass<SomeModel>;
```

:::warning

Models are instantiated once per process, and their instances are then cached. Do not expect constructor or `init` hook calls on every model load.

:::

:::tip

Please do not use the plural form for model names.

**Bad** - Coin**s**

**Good** - Coin

:::

:::tip Annotating `this` on instance methods

An instance method may declare an explicit `this:` to type its body — handy when
the body assumes a narrower shape than the raw document (a populated ref, a
non-null [plugin-reshaped field](#typing-plugin-reshaped-fields), sibling
methods):

```ts
getInfo: async function (this: SomeModelLite) {
  return { _id: this._id, email: this.email };
},
```

That annotation types the **body only**. You still call the method directly on
the document — `doc.getInfo()` — on any model handle; the framework drops the
authored `this` from the caller-facing type, since a method accessed on its own
document always has the right `this` at runtime. No `(schema.methods.x as …)
.call(doc, …)` cast is needed.

:::

## Typing plugin-reshaped fields

Some Mongoose plugins reshape a field's value at runtime: `mongoose-intl` turns a
`String` field into a `{ native, machine }` sub-document, an encryption plugin
swaps a string for a cipher object, a custom getter returns a different type. The
framework infers a field's type from `type:` (here, `string`), so the static type
no longer matches what is actually stored — and you end up casting at every read.

Mark such a field with `TsTypeOverride<T>` to declare its real compile-time type.
The marker is a phantom (`__tsType`, never set at runtime), so the plugin keeps
doing the reshaping; only the static type changes.

```ts title="/src/models/Event.ts"
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";
import type { TsTypeOverride } from "@adaptivestone/framework/modules/BaseModel.js";
import type { IntlSubDocValue } from "mongoose-intl"; // your plugin's value type

// A small factory keeps schemas readable: a `String` field the intl plugin
// reshapes into an `IntlSubDocValue` at runtime.
function intlString<C extends object>(field: C) {
  return field as C & TsTypeOverride<IntlSubDocValue<string>>;
}

export default class Event extends BaseModel {
  static get modelSchema() {
    return {
      title: intlString({ type: String, intl: true }),
      schedule: [{ title: intlString({ type: String, intl: true }) }],
      plainField: { type: String }, // unmarked → still `string`
    } as const;
  }
}
```

The static type now follows the runtime value everywhere — no casts:

```ts
const Event = this.app.getModel("Event");
const event = await Event.findOne();
event?.title?.native; // `title` is IntlSubDocValue<string>
event?.schedule?.[0]?.title?.machine; // any depth (nested + subdoc arrays)
event?.plainField; // unmarked field is still `string`
```

:::note

The override is **opt-in** and a strict **no-op** for any field without the
marker — existing models are unaffected. It recurses into nested objects and
subdocument arrays, so a reshaped field can appear at any depth. The same marker
works for any runtime-reshaping plugin (encrypted fields, custom getters, …), not
just `mongoose-intl`.

:::

## Typing populated references

A reference field (`{ type: Schema.Types.ObjectId, ref: "User" }`) is typed as an
`ObjectId` — that is what is stored, and what you get back when the field is
**not** populated. After `.populate(...)` the runtime value is the referenced
document, but the inferred type stays `ObjectId` (Mongoose cannot know at the
schema level which queries populate it). There are two cast-free ways to type the
populated value, depending on how often you populate the field.

**Per call — `.populate<T>()`.** When you populate occasionally, pass the
populated shape as the type argument at the call site. The returned document is
typed with that field replaced:

```ts
const Boat = this.app.getModel("Boat");
const boat = await Boat.findOne();
const populated = await boat!.populate<{ owner: { email: string } }>("owner");
populated.owner.email; // typed — no cast
```

**Always — mark the field.** When a field is almost always read populated, mark it
with `TsTypeOverride` as the **union** of both states (`ObjectId` when not
populated, the document when it is). Reads then narrow without a cast:

```ts title="/src/models/Boat.ts"
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";
import type { TsTypeOverride } from "@adaptivestone/framework/modules/BaseModel.js";
import { Schema, type Types } from "mongoose";

type PopulatedOwner = { email: string; name: string };

function ref<C extends object, T>(field: C) {
  return field as C & TsTypeOverride<Types.ObjectId | T>;
}

export default class Boat extends BaseModel {
  static get modelSchema() {
    return {
      owner: ref<{ type: typeof Schema.Types.ObjectId; ref: "User" }, PopulatedOwner>({
        type: Schema.Types.ObjectId,
        ref: "User",
      }),
    } as const;
  }
}
```

```ts
const boat = await this.app.getModel("Boat").findOne();
// `owner` is `ObjectId | PopulatedOwner | undefined` — narrow before use:
if (boat?.owner && "email" in boat.owner) {
  boat.owner.email; // typed as PopulatedOwner
}
```

:::note

Refs that are not marked stay plain `ObjectId`, and `.populate<T>()` always works
regardless. Prefer the marker only for fields you consistently populate — the
union forces a narrowing check, which is the honest cost of a field that is
sometimes an id and sometimes a document.

:::

## API

```ts
getModel(modelName: string): MongooseModel<any>;
```

Example:

```js
const UserModel = this.app.getModel("User");
const userInstance = await UserModel.findOne({ email: "user@email.com" });
```

## Configuration

The main configuration variable is the `MONGO_DSN` environment variable, which the model uses to connect to the database.

## Built-in Models

The framework comes with a few built-in models.

### User

It is part of the authorization system and handles user storage, password hashing, and provides basic functions for token generation and user retrieval.

If you want to create your own user implementation, you should override or disable this one.

The authentication controller depends on this model.

#### API

```js
const UserModel = this.app.getModel("User");
const user = await UserModel.getUserByEmailAndPassword("email", "password");
const userToken = await user.generateToken(); // Generates and stores a token in the database
const userPublic = await user.getPublic();
// `hashPassword` is a standalone helper, not a model static:
// import { hashPassword } from "@adaptivestone/framework/helpers/crypto.js";
const hashedPassword = await hashPassword("password");
const sameUser = await UserModel.getUserByToken(userToken);
const sameUserAgain = await UserModel.getUserByEmail(user.email);
// The token generators live in `userHelpers`, not on the model:
// import { userHelpers } from "@adaptivestone/framework/models/User.js";
const recoveryToken = await userHelpers.generateUserPasswordRecoveryToken(user);
const sameUserAgain2 = await UserModel.getUserByPasswordRecoveryToken(
  recoveryToken
);
const isSuccess = await user.sendPasswordRecoveryEmail(i18n);
const verificationToken = await userHelpers.generateUserVerificationToken(user);
const sameUserAgain3 = await UserModel.getUserByVerificationToken(
  verificationToken
);
const isSuccess2 = await user.sendVerificationEmail(i18n);
```

#### Customizing the User model

To replace the framework's `User`, drop your own `User.ts` into your project's
`models/` folder. The [inheritance process](03-files-inheritance.md) makes it win
over the framework's, and `getModel("User")` / `req.appInfo.user` are typed
against **your** model automatically (run `generatetypes` after adding it).

There are two ways to customize it.

**Add fields** — extend the framework's `User` and spread its schema:

```ts title="/src/models/User.ts"
import FrameworkUser from "@adaptivestone/framework/models/User.js";

export default class User extends FrameworkUser {
  static get modelSchema() {
    return {
      ...FrameworkUser.modelSchema,
      company: { type: String },
    } as const;
  }
}
```

The inherited auth statics and instance methods (`getUserByEmailAndPassword`,
`generateToken`, `getPublic`, …) keep working on your model with no casts.

**Reshape fields** — when you need to change a field's _shape_ (for example an
i18n `name`, or a singular `role` instead of `roles[]`), TypeScript can't express
a type _replacement_ through `extends` (the static-getter override is checked
covariantly, so it fails with `TS2417`). Compose instead: extend `BaseModel` and
reuse the framework's auth logic by spreading it in.

```ts title="/src/models/User.ts"
import { BaseModel } from "@adaptivestone/framework/modules/BaseModel.js";
import FrameworkUser from "@adaptivestone/framework/models/User.js";
import type { Schema } from "mongoose";

export default class User extends BaseModel {
  static get modelSchema() {
    return {
      name: { native: { type: String }, machine: { type: String } },
      email: { type: String },
      password: String,
      sessionTokens: [{ token: String, valid: Date }],
      role: { type: String },
      // …the rest of your schema
    } as const;
  }

  static get modelStatics() {
    return { ...FrameworkUser.modelStatics } as const;
  }

  static get modelInstanceMethods() {
    return { ...FrameworkUser.modelInstanceMethods } as const;
  }

  static initHooks(schema: Schema) {
    FrameworkUser.initHooks(schema); // keeps the password-hashing pre-save hook
  }
}
```

The shipped auth helpers are typed against small structural contracts
(`UserAuthDoc` / `UserAuthInstance` / `UserAuthModel`), so they stay callable on
your reshaped model without casts.

:::note

The auth statics (`getUserByEmailAndPassword`, `getUserByToken`, …) only read a
few fields — `email`, `password`, and the token arrays. Any model that keeps
those reuses them as-is. `getPublic` returns the framework's public shape, so
override it if your model reshapes the fields it reads (such as `name`).

:::

### Migration

The migration model is a helper for the migration subsystem. It stores the names of migrated files to ensure that each migration is only executed once.

Please refer to the `CLI/migrations` section for more details.

You should probably not use this model directly.

### Sequence

The Sequence model allows you to generate sequences by name. This is a cross-server-safe method for generating sequences in a distributed environment.

```javascript
const SequenceModel = this.app.getModel("Sequence");
// Will be 1.
const someTypeSequence = await SequenceModel.getSequence("someType");
// Will be 2.
const someTypeSequence2 = await SequenceModel.getSequence("someType");
// Will be 1, as the type is different.
const someAnotherTypeSequence = await SequenceModel.getSequence(
  "someAnotherType"
);
```

### Lock

The Lock model provides the ability to lock resources in a distributed environment.

This can be used for external requests, system actions, etc.

Imagine you have a high volume of traffic requesting data from an external system. You also have a cache for this data, but you must initially query the internal API to retrieve it. To prevent overwhelming the API, you want to ensure that you only request the data once and that other simultaneous requests wait for the result instead of making redundant calls. This is where the Lock model can help.

```javascript

const LockModel = this.app.getModel("Lock");

  /**
   * Acquires a lock based on the lock name.
   * @param {string} name
   * @param {number} [ttlSeconds=30]
   * @returns {Promise<boolean>}
   */
  async acquireLock(name, ttlSeconds = 30)

  /**
   * Releases a lock based on the lock name.
   * @param {string} name
   * @returns {Promise<boolean>}
   */
  async releaseLock(name)

  /**
   * Waits for a lock based on the lock name.
   * @param {string} name
   * @returns {Promise}
   */
  async waitForUnlock(name)

  /**
   * Gets the lock's remaining time based on the lock name.
   * @param {string} name
   * @returns {Promise<{ttl: number}>}
   */
  async getLockData(name)


  /**
   * Gets the locks' remaining time based on the lock names.
   * @param {string[]} names
   * @returns {Promise<{name: string, ttl: number}[]>}
   */
  static async getLocksData(names)

```

Example of usage:

```javascript

async someHTTPRequestWithExpensiveExternalAPI(req, res) {
  // We have some external requests, which can be simultaneous requests from different users.
  const LockModel = this.app.getModel("Lock");
  // Let's say it's AI processing of a video, for example.
  const { videoId } = req.appInfo.request;

  // Check if we already have it.
  const VideoAIModel = this.app.getModel("VideoAIModel");
  const videoAI = await VideoAIModel.findOne({ videoId });
  if (videoAI) {
    return res.json(videoAI.getPublic());
  }

  const lockName = `video-ai-processing-${videoId}`;

  // We don't have that video, so let's send it for processing using a lock.
  const isLockAcquired = await LockModel.acquireLock(lockName);
  if (isLockAcquired) {
    const result = await videoAIService.processVideo(videoId);
    const videoModel = await VideoAIModel.create({ videoId, result });
    // Release the lock.
    await LockModel.releaseLock(lockName);
    // Return the result.
    return res.json(videoModel.getPublic());
  }

  // We don't have a lock, so let's wait for one.
  await LockModel.waitForUnlock(lockName);
  // It looks like the external process is finished, so let's check for the result.
  const videoAI2 = await VideoAIModel.findOne({ videoId });
  if (videoAI2) {
    return res.json(videoAI.getPublic());
  }

  // If there's no result, we'll return an error.
  return res.status(500).json({ error: "Something went wrong" });
}
```

### KeyValue

A minimal persistent key/value store backed by MongoDB. Think of it as a tiny, shared "settings drawer" for your app: a place to keep small pieces of state that should survive restarts and be readable by every process — a lightweight cache, runtime configuration, feature flags, the cursor of a background job, and so on.

The model is intentionally schema-only — it adds no custom methods. The key is the document `_id` (a string), and the value is a `Mixed` field, so it can hold anything Mongoose can serialise (string, number, boolean, array, or nested object). You interact with it through the standard Mongoose API that every model already exposes.

```ts
static get modelSchema() {
  return {
    _id: { type: String, required: true },
    value: { type: Schema.Types.Mixed, required: true },
  } as const;
}
```

#### Usage

```js
const KeyValue = this.app.getModel("KeyValue");

// Set (create or overwrite). `upsert: true` makes it idempotent.
await KeyValue.findByIdAndUpdate(
  "config:theme",
  { value: "dark" },
  { upsert: true },
);

// Get. Returns the document or `null` when the key is missing.
const doc = await KeyValue.findById("config:theme");
const theme = doc?.value ?? "light"; // fall back to a default

// Any serialisable value works.
await KeyValue.findByIdAndUpdate(
  "config:features",
  { value: { newDashboard: true, limits: [10, 50, 100] } },
  { upsert: true },
);

// Read many keys at once.
const docs = await KeyValue.find({ _id: { $in: ["config:theme", "config:features"] } });
const map = new Map(docs.map((d) => [d._id, d.value]));

// Delete.
await KeyValue.deleteOne({ _id: "config:theme" });
```

:::tip

Use a `namespace:key` convention for the `_id` (for example `config:theme`, `cache:user-42`, `flag:beta-signup`). It keeps keys readable and makes prefix queries with a regular expression easy:

```js
const allConfig = await KeyValue.find({ _id: /^config:/ });
```

:::

#### Caching pattern

Because every process reads the same collection, `KeyValue` is a convenient cross-server cache for values that are expensive to compute but cheap to store.

```js
async function getExchangeRates(app) {
  const KeyValue = app.getModel("KeyValue");
  const cached = await KeyValue.findById("cache:exchange-rates");
  if (cached) {
    return cached.value;
  }

  const rates = await fetchExpensiveRatesFromExternalApi();
  await KeyValue.findByIdAndUpdate(
    "cache:exchange-rates",
    { value: rates },
    { upsert: true },
  );
  return rates;
}
```

Pair it with the [`Lock`](#lock) model when several requests might try to populate the same cache key at once, so the expensive work runs only once.

:::note

`KeyValue` is **persistent storage**, not an expiring cache — entries live until you delete them. There is no built-in time-to-live. If you need automatic expiration, add an `expireAt` date field and a TTL index in `initHooks`, the same way the `Lock` model does:

```ts
static initHooks(schema: Schema) {
  schema.index({ expireAt: 1 }, { expireAfterSeconds: 0 });
}
```

For request-scoped or in-memory caching, see the [Cache](11-cache.md) section instead.

:::

#### Concurrency

`value` is a `Mixed` field, so it is replaced as a whole — concurrent writers are last-write-wins. Do not read a value, mutate it in your code, and write it back if multiple processes update the same key; you may lose updates. For counters or fields that must change atomically, use MongoDB update operators directly (`$inc`, `$set` on a sub-path) or reach for the [`Sequence`](#sequence) model.


---


# Document 6: 05-modelsOld

<!-- Source: 05-modelsOld.md -->

# Models (OLD)

:::warning

This documentation is for old model definitions. Please do not use this type of models at all. Use current model versions.
:::

Framework based on [mongoose](https://mongoosejs.com/) library and provide direct access to mongoose.

It uses [ES6 class variant](https://mongoosejs.com/docs/guide.html#es6-classes) of mongoose to init. Also you can access framework

Model take care about database connection

:::note

Models files part of [framework inheritance process ](03-files-inheritance.md).

:::

## Access mongoose instance

Inside the class mongoose is available as

```js
this.mongooseModel;
```

## Basic model

```js
import AbstractModel from "@adaptivestone/framework/modules/AbstractModel.js";

class SomeModel extends AbstractModel {
  constructor(app) {
    super(app);
    // you can put some init stuff
  }

  initHooks() {
    super.initHooks();
    // place to init plugins, indexes, etc.
    // As it happens after loaded class into mongoos, but before mongoose inited class
    // this.mongooseSchema.plugin(PLUGIN_NAME);
  }

  // here mongoose scheme go
  // this is a full mongoose schema
  // Please refer to mongoose documentation https://mongoosejs.com/docs/guide.html
  get modelSchema() {
    return {
      someString: { type: String, required: true },
      firstName: String,
      lastName: String,
      email: String,
    };
  }

  // here mongoose scheme options go
  // this is a full mongoose schema
  // Please refer to mongoose documentation https://mongoosejs.com/docs/guide.html#options
  get modelSchemaOptions() {
    return {
      read: "primary",
    };
  }

  // Static method will be part of the mongoose class
  // this.app.getModel('SomeModel').someStaticMethod()
  static async someStaticMethod() {
    const somedata = await this
      .findByIdAndUpdate
      //.....
      ();
    const { app } = this.getSuper();
    return somedata;
  }

  // any methods will be part on instance method
  // await this.app.getModel('SomeModel').findById(124).someInstanceMethod()
  async someInstanceMethod() {
    // you can access app into the instance method
    const { app } = this.getSuper();

    // inside of instance method you can access model data
    this.someString;
  }

  // any getters will became a mongoose virtual
  // const SomeModels = this.app.getModel("SomeModel").
  // const someModelInstance = await SomeModel().create({ email: 'test@gmail.com' });;
  // `domain` is now a property on SomeModels documents.
  // someModelInstance.domain; // 'gmail.com'
  get domain() {
    return this.email.slice(this.email.indexOf("@") + 1);
  }

  // setters also be an virtual
  // const SomeModels = this.app.getModel("SomeModel").
  // const someModelInstance = new SomeModel();
  // Vanilla JavaScript assignment triggers the setter
  // someModelInstance.fullName = 'Jean-Luc Picard';
  set fullName(v) {
    // `v` is the value being set, so use the value to set
    // `firstName` and `lastName`.
    const firstName = v.substring(0, v.indexOf(" "));
    const lastName = v.substring(v.indexOf(" ") + 1);
    this.set({ firstName, lastName });
  }
}

export default SomeModel;
```

:::tip

If you have some relations ("ref") on a mongoose model that you should care to load schema. As mongoose can only build relationships with schemas in memory. Google place to do that - inside constructor. Be aware on loop linking models

```js
constructor(app){
	super(app);
	this.app.getModel(“ReferenceModelName”);
}
```

:::

:::warning

Models united onse per process and then united instances cached. Do NOT expect constructor or init hook calls on every model loading

:::

:::tip

Please do not name the model in plural form.

**Bad** - Coin**s**

**Good** - Coin

:::

## API

```js
getModel(modelName: string): MongooseModel<any>;
```

Example:

```js
const UserModel = this.app.getModel("User");
const userInstance = await UserModel.findOne({ email: "user@email.com" });
```

## Configuration

Main configuration variable "MONGO_DSN" environment variable. Based on it model will made connection to the database

## Built-in models

Framework came with few built-in models.

### User

It's a part of the authorization system. It takes care of storing users, hash passwords and provides some basic functions for token generation and getting users.

If you want to have your own user implementation you should overwrite it or disable.

Auth controller depends on this model

#### API

```js
const UserModel = this.app.getModel("User");
const user = await UserModel.getUserByEmailAndPassword("email","password");
const userToken = await user.generateToken(); // generate and store token in the database
const userPublic = await user.getPublic();
const hashedPassword = await UserModel.hashPassword("password");
const sameUser = await UserModel.getUserByToken(userToken);
const sameUserAgain = await UserModel.getUserByEmail(user.email);
const recoveryToken = await UserModel.generateUserPasswordRecoveryToken(user);
const sameUSerAgain2 = await UserModel.getUserByPasswordRecoveryToken(recoveryToken);
const isSuccess = await user.sendPasswordRecoveryEmail(i18n);
const verificationToken = await UserModel.generateUserVerificationToken(user);
const sameUserAgain3 = await UserModel.getUserByVerificationToken(verificationToken);
const isSuccess2 = await user.sendVerificationEmail(i18n);
```

### Migration

Migration model it's helper for migration subsystems. It stores migrated files to make sure that migrated executed once

Please refer to CLI/migrations for more details

You probably should not use this model directly

### Sequence

Sequence allows you to generate sequences by name. This is cross server safe method to generate sequences in distributed environment

```javascript
const SequenceModel = this.app.getModel("Sequence");
// will be 1
const someTypeSequence = await SequenceModel.getSequence("someType");
// will be 2
const someTypeSequence2 = await SequenceModel.getSequence("someType");
// will be 1 as type is another
const someAnotherTypeSequence = await SequenceModel.getSequence(
  "someAnotherType"
);
```

### Lock

Lock model is designed to provide ability to lock some resources in distributed environment.

This can be external requests, some actions in the system, etc.

Imagine that you have a log of traffic that asks external system for some data. You have cache of this data as weel, but initially you shaul asks internal api to get this data. And you want to make sure that you asking this api for that data only one time and other same time requests will wait for the result instead of asking api. This is where Lock model can help you.

```javascript

const LockModel = this.app.getModel("Lock");

  /**
   * acquire lock based on lock name
   * @param {string} name
   * @param {number} [ttlSeconds=30]
   * @returns {Promise<boolean>}
   */
  async acquireLock(name, ttlSeconds = 30)

  /**
   * release lock based on lock name
   * @param {string} name
   * @returns {Promise<boolean>}
   */
  async releaseLock(name)

  /**
   * wait lock based on lock name
   * @param {string} name
   * @returns {Promise}
   */
  async waitForUnlock(name)

  /**
   * get lock remaining time based on lock name
   * @param {string} name
   * @returns {Promise<{ttl: number}>}
   */
  async getLockData(name)


  /**
   * get lock remaining time based on lock name
   * @param {string[]} names
   * @returns {Promise<{name: string, ttl: number}[]>}
   */
  static async getLocksData(names)

```

Example of usage:

```javascript

async someHTTPRequestWithExpensiveExternalAPI(req,res){
  // we have some external request (this can be same time request from different users)
  const LockModel = this.app.getModel("Lock");
  // lets say its a AI processing of video (for example)
  const {videoId} = req.appInfo.request;

  // check if we already have it
  const VideoAIModel = this.app.getModel("VideoAIModel");
  const videoAI = await VideoAIModel.findOne({videoId});
  if(videoAI){
    return res.json(videoAI.getPublic());
  }

  const lockName = `video-ai-processing-${videoId}`;

  // we have no that video, lets send it otp rocessing with lock
  const isLockAcquired = await LockModel.acquireLock(lockName);
  if(isLockAcquired){
    const result = await videoAIService.processVideo(videoId);
    const videoModel = await VideoAIModel.create({videoId, result});
    // release lock
    await LockModel.releaseLock(lockName);
    // return result
    return res.json(videoModel.getPublic());
  }

  // we have no lock, lets wait for it
  await LockModel.waitForUnlock(lockName);
  // lookslikeexternal process is done, lets check if we have result
  const videoAI2 = await VideoAIModel.findOne({videoId});
  if(videoAI2){
    return res.json(videoAI.getPublic());
  }

  // we have no result, lets return error
  return res.status(500).json({error: "something went wrong"});
}

```


---


# Document 7: 06-Controllers > 01-intro

<!-- Source: 06-Controllers/01-intro.md -->

# Controllers

Controllers are a crucial component of the framework. The framework uses [Express.js](https://expressjs.com/) for the HTTP lifecycle (listening, body parsing, response API, the third-party middleware ecosystem) and a tree-based `RouteRegistry` for path matching, parameter extraction, method dispatch, and middleware ordering. Controllers contribute subtrees to the global registry — auto-loaded from the controllers folder, mounted via a single Express middleware.

:::note

Controller files are part of the [framework inheritance process](03-files-inheritance.md).

:::

The framework provides built-in error handling, automatic controller loading (including from subfolders), and request validation and casting.

:::note

In production, the framework uses the `cluster` module to start multiple instances (based on the number of CPU cores) and provide load balancing between them.
Keep in mind that you cannot access one process from another. For complex scenarios (like a WebSocket server), you will need to use inter-process communication techniques to send messages between processes.
:::

## Controller Structure

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    // Return routes info.
    // NECESSARY part.
  }

  getHttpPath() {
    // URL prefix for this controller. Default: `/{constructor-name-lowercase}`.
    // Override in a subclass to customize (e.g., `Home` → `/`).
  }

  static get middleware() {
    return new Map();
    // Path-/method-scoped middlewares for routes in THIS controller.
  }
}
export default ControllerName;
```

:::tip

Only the `routes` getter is required; other parts of the controller can be omitted if not needed.
:::

:::warning

Controllers should extend the "AbstractController" module.
:::

## Name Convention and Loading

The framework will load any file (except for `*.test.js` and `*.test.ts` files) and initialize it as an HTTP module. By default, the filename will be used as the route name. This behavior can be customized by providing your own `getHttpPath` function.

### Explicit registration

In addition to file-based auto-loading, you can register a controller programmatically via `app.controllerManager.registerController(ControllerClass, prefix?)`. This is useful for:

- **Test fixtures** — register a controller only for a specific test (see the [Testing](../09-testsing.md) chapter).
- **Late registration** — controllers added after `Server.startServer()` boots, via the `callbackBefore404` hook so routes mount before the 404 handler.
- **Conditional controllers** — only register based on config, feature flags, or runtime detection.

```js
import MyController from "./controllers/MyController.js";

await server.startServer(async () => {
  // Runs after framework controllers init, before the 404 handler.
  // Routes mount on `/my/mycontroller/*` (prefix + lowercase class name).
  server.app.controllerManager?.registerController(MyController, "my");
});
```

Auto-loading internally uses the same `registerController` entry point — both paths produce identical instances.

For the example above:

```js
class ControllerName extends AbstractController {
```

The route will be “http://localhost:3300/controllername”.

Then, any method from the router will be accessible via the URL.

If you want to define a custom path, you can provide your own implementation of the `getHttp-Path` function.

```js
  getHttpPath() {
    return "superDuperMegaSpecialRoute";
  }
```

By default, `getHttpPath` resolves the current folder and filename and uses them to construct the route name.

### Project boot hook (`bootHttp`)

For app-wide HTTP wiring that doesn't belong to any single controller — webhooks, healthchecks, OAuth callbacks, or boot-time setup — pass a **`bootHttp`** function to the `Server` constructor. The framework calls it with the live `app` during `startServer`, after controllers are registered but before the adapter mounts, so anything it adds is in place from the first request.

Define it inline — `app` is inferred, no annotation needed:

```ts title="src/index.ts"
import Server from "@adaptivestone/framework/server.js";

const server = new Server({
  folders: {
    /* … */
  },
  bootHttp: async (app) => {
    // `httpServer` is set by the time bootHttp runs (typed nullable, so use `?.`).
    // An ad-hoc route that doesn't fit the controller convention:
    app.httpServer?.routeRegistry.registerRoute("POST", "/webhooks/stripe", {
      handler: stripeWebhookHandler,
    });

    // Or app-wide Express middleware (runs before the router):
    // app.httpServer?.express.use(myGlobalMiddleware);
  },
});
await server.startServer();
```

There's no required file or folder for it — it's just a function. If it grows, extract it to its own module and type it with `BootHttpHook`:

```ts title="src/bootHttp.ts"
import type { BootHttpHook } from "@adaptivestone/framework/server.js";

const bootHttp: BootHttpHook = async (app) => {
  /* … */
};

export default bootHttp;
```

It's wired **explicitly**, not auto-discovered from a folder: the framework finds everything else through configured folders, and those are all spoken for — `config/` merges its files as config, `controllers/` auto-loads its files as controllers — so there's no conflict-free folder to scan. `bootHttp` is **HTTP-specific** by design: it needs `app.httpServer`, which only exists once the HTTP server boots (CLI and worker processes never run it). Unlike the `callbackBefore404` hook (which you pass to `startServer` and which runs _after_ the adapter mounts), `bootHttp` runs _before_ the mount.

## Request Flow

![RequestFlow](/img/requestFlow.jpg)

## View

:::warning

Built-in view rendering was **removed in version 5**. The framework no longer ships a `views/` folder or a default template engine — it is API-first (returns JSON).

:::

If you need server-rendered HTML, the underlying Express 5 instance is available on `app.httpServer.express` (once the HTTP server has booted), so you can register a template engine yourself (see the [Express template-engine guide](https://expressjs.com/en/guide/using-template-engines.html)). For most applications, return JSON and render on the client.

## JSON

JSON is the most common data format for modern web applications, but its flexibility can sometimes lead to confusion.

We provide [basic documentation](https://andrey-systerr.notion.site/API-JSON-41f2032055ae4bddae5d033dc28eb1d3) on how we recommend working with JSON, and the framework is designed to follow these guidelines.

## Configuration

The configuration file is located at `config/http.js`.

Please take a moment to review it.

The most notable options are:

```js
port; // Port that the server will use. By default, process.env.HTTP_PORT or port 3300.
hostname; // IP to bind to. By default, process.env.HTTP_HOST or '0.0.0.0' (any). Could be dangerous.
corsDomains; // CORS-allowed domains.
```


---


# Document 8: 06-Controllers > 02-routes

<!-- Source: 06-Controllers/02-routes.md -->

# Routes

Routes are relative to the controller route. You SHOULD NOT use the full route here.

Route objects have multiple levels.

## Route First Level (Method Level)

On the first level, only the ‘method’ (post, put, delete, etc.) exists. Only requests with these methods will go deeper into the real routes.

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      post: {
        // post routes
      },
      get: {
        // get routes
      },
      put: {
        // put routes
      },
      // etc.
    };
  }
}
export default ControllerName;
```

## Route Second Level (Path Level)

Inside the methods (second level), we have a path. The framework's tree-based router supports a small, opinionated set of patterns:

```js
"/fullpath"                          // literal path
"/fullpath/:paramOne/:paramTwo"      // named params → req.params.paramOne, paramTwo
"/api/{*rest}"                       // catch-all splat → req.params.rest = "/v1/users/42"
```

| Syntax | Matches | Captures |
|---|---|---|
| `/literal` | exact segment | nothing |
| `:name` | exactly one segment | `req.params.name` |
| `{*name}` | zero or more segments to end of path | `req.params.name` (joined with `/`) |

**Specificity** (when patterns overlap): static segments win, then `:param`, then `{*splat}`. So `/users/me` registered alongside `/users/:id` always matches the literal first.

**URL decoding** is per-segment (Spring `PathPatternParser` model). `%2F` inside a `:param` value stays as `/`; the matcher does not split on it. For `{*splat}` captures, segments are decoded individually then re-joined — encoded-slash distinction is lost (documented trade-off; use raw-body mode for the rare encoded-slash case).

**Defaults**: case-insensitive, lenient trailing slash. Both flip in v6.

:::note

The order of routes matters when patterns overlap at the same specificity tier (e.g., two `:param` siblings). The first matched route is executed.
:::

Example:

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      post: {
        "/someUrl": {
          handler: this.postSomeUrl,
          request: yup.object().shape({
            count: yup.number().max(100).required(),
          }),
        },
      },
    };
  }
}
export default ControllerName;
```

## Route Third Level (Route Object Level)

On the third level, we have a "route object," a special object that will describe our route.

```js
{
  handler: this.postSomeUrl, // required
  request: yup.object().shape({ // optional
    count: yup.number().max(100).required(),
  }),
  query: yup.object().shape({ // optional
    page: yup.number().required(),
  }),
  middleware: [RateLimiter], // optional
  description: "Create a sample" // optional — a plain string; becomes the OpenAPI operation summary
}

```

Here:

```js
Handler; // Some async function (most likely in this controller file) that will do all the work.
Request; // A special interface that will do validation of body parameters for you.
Query; // A special interface that will do validation of query parameters for you.
Middleware; // An array of middlewares specially for the current route.
Description; // A description of this route — becomes the OpenAPI operation summary.
```

The route `description` together with the `request:` / `query:` schemas and the middleware chain are what the framework reads to generate an [OpenAPI 3.1 document](../17-openapi.md).

## Request

Request does validation and casting of an upcoming `req.body`.

As we want to use already well-defined solutions, we believe that [yup](https://github.com/jquense/yup) is a great example of how a schema should be validated.

But you still have the ability to provide your own validation based on an interface.

:::warning
Request works on a body level.
:::

Request contains all fields from `req.body` and passes them into validation.

:::warning
Please note that GET methods have no BODY.
:::

Parameters after validation are available as `req.appInfo.request`.

:::warning
Do not use `req.body` directly. Always use parameters via `req.appInfo.request`.

:::

## Query

Query does validation and casting of an upcoming `req.query`.

The Yup schema is described similarly to the request.

:::warning
Query works on a query level.
:::

Query contains all fields from `req.query` and passes them into validation.

Parameters after validation are available as `req.appInfo.query`.

:::warning
Do not use `req.query` directly. Always use parameters via `req.appInfo.query`.

:::

## Validation

The framework dispatches validation through [Standard Schema](https://standardschema.dev/) — a vendor-neutral interface. Any conforming validator works as a route's `request:` or `query:` schema with no glue code:

| Validator | Standard Schema support |
|---|---|
| [Yup](https://github.com/jquense/yup) | ≥1.7 |
| [Zod](https://zod.dev/) | ≥3.24 |
| [Valibot](https://valibot.dev/) | all current versions |
| [ArkType](https://arktype.io/) | all current versions |

Yup is shown in the examples below since the framework historically taught it, but the same shapes are accepted from any Standard Schema-conforming library.

:::note
`request:` validates the request **body** and `query:` validates the **query string** — **path params (`:id`) are not validated**. A raw param passed to Mongoose (`findById(req.params.id)`) throws a `CastError` → 500 on a malformed id. Guard params yourself — see [Recipes → Validate an ObjectId](../15-recipes.md#validate-an-objectid).
:::

:::note
The framework no longer bundles a validator. `yup` is an **optional peer dependency** — install it only if you use yup schemas (or the deprecated `YupFile`). For dependency-free validation of simple shapes, use [`defineSchema`](#zero-dependency-schemas-defineschema) below.
:::

### Yup example

```js
request: yup.object().shape({
  count: yup.number().max(100).required("error text"),
});
query: yup.object().shape({
  page: yup.number(),
});
```

A more complete example:

```js
request: yup.object().shape({
  name: yup.string().required("validation.name"), // You can use i18n keys here.
  email: yup.string().email().required("Email required field"), // Or just text.
  message: yup
    .string()
    .required("Message required field")
    .min(30, "minimum 30 chars"), // Additional validators for different types exist.
  pin: yup.number().integer().min(1000).max(9999).required("pin.pinProvided"),
  status: yup
    .string()
    .required("Status required field")
    .oneOf(["WAITING", "CANCELED"]), // One of.
  transaction: yup
    .object() // Deep-level object.
    .shape({
      to: yup.string().required(),
      amount: yup.number().required(),
      coinName: yup.string().oneOf(["btc", "etc"]).default("etc"), // Default.
    })
    .required(),
});
```

For Yup schemas, the framework automatically strips unknown fields (security-relevant when handlers spread `req.appInfo.request` into model creates). You don't need `.noUnknown()` — the framework calls `cast(data, { stripUnknown: true })` for you.

### Zod example

```js
import { z } from "zod";

request: z.object({
  count: z.number().max(100),
  message: z.string().min(30, "minimum 30 chars"),
});
query: z.object({
  page: z.number().optional(),
});
```

Zod (and Valibot, ArkType) strip unknown fields by default — no extra configuration needed. To allow unknown fields explicitly, use the library's pass-through API (Zod's `.passthrough()`, Valibot's `looseObject`, ArkType's `'+': 'ignore'`).

### Zero-dependency schemas (`defineSchema`)

When you want a small schema without pulling in a validator library, use `defineSchema` — a one-function adapter that wraps a plain validate callback into a Standard Schema object:

```ts
import { defineSchema } from "@adaptivestone/framework/services/validate/defineSchema.js";

request: defineSchema<{ email: string }>((value) => {
  const v = (value ?? {}) as Record<string, unknown>;
  if (typeof v.email !== "string" || !v.email.includes("@")) {
    return { issues: [{ message: "validation.email", path: ["email"] }] };
  }
  // Return only known keys — unknown input is stripped by construction.
  return { value: { email: v.email } };
});
```

- The `Output` generic (`{ email: string }`) is what the codegen reads for the typed handler signature (`req.appInfo.request`) — see below. You declare it; the runtime checks live in the callback.
- Return `{ value }` on success (only the keys you copy survive — this is your strip-unknown), or `{ issues }` on failure. Each issue's `message` is an i18n key (or literal text); the framework auto-translates it like any other validator.
- This is the mechanism the built-in `Auth` controller uses, so the framework itself ships validator-free.

`defineSchema` is intentionally minimal — there are no `string()`/`object()` combinators. If you find yourself hand-writing many or deeply nested schemas, reach for a real validator library (Zod, etc.) instead.

### Typed handler signatures (codegen)

Running `npm run gen` (alias for `npm run cli generatetypes`) emits a `<File>.routes.gen.ts` next to every controller. The gen file exports one type alias per handler method (PascalCase suffixed with `Request`) — handlers import the alias instead of hand-writing `FrameworkRequest & { appInfo: { request: { ... } } }`:

```ts
// src/controllers/Auth.ts
import type { PostLoginRequest } from "./Auth.routes.gen.ts";
import { object, string } from "yup";

class Auth extends AbstractController {
  get routes() {
    return {
      post: {
        "/login": {
          handler: this.postLogin,
          request: object().shape({
            email: string().email().required(),
            password: string().required(),
          }),
        },
      },
    };
  }

  async postLogin(req: PostLoginRequest, res: Response) {
    // req.appInfo.request.email is typed as string (from the schema)
    // req.appInfo.user is typed as InstanceType<TUser> | undefined
    //   (from GetUserByToken middleware's `static get provides()`)
    // req.appInfo.i18n.t(...) is typed (from BaseAppInfo)
  }
}
```

The gen file uses `InstanceType<typeof Controller>['routes'][...]['request']` type navigation, so schemas stay inline in the `routes` getter — no extracted named consts required. It also intersects in `provides` shapes from the middleware tuple at this route, so `req.appInfo.user` (and any other middleware-contributed fields) are typed automatically.

**What the generated type carries** (per route):

| Source | Emitted into |
|---|---|
| Route path `:name` segments (e.g., `/users/:id`) | `req.params: { id: string }` |
| Route path `{*name}` splats (e.g., `/files/{*path}`) | `req.params: { path: string }` — splats join captured segments with `/` (one string, not an array) |
| Route `request:` schema | `req.appInfo.request: StandardSchemaV1.InferOutput<...>` |
| Route `query:` schema | `req.appInfo.query: StandardSchemaV1.InferOutput<...>` |
| Middleware-chain `static get provides()` returns | Merged into `req.appInfo` |

The middleware chain in the gen file is read from the same `RouteRegistry.flatten()` the runtime uses — so the types you see at compile time match the middlewares that actually run at request time. No parallel matcher to drift.

#### Keep `routes` declarative (codegen reads the source, never your constructor)

`npm run gen` analyzes each controller **statically, from its source AST** — it never imports, constructs, or runs your controller. That keeps type generation free of side effects (config reads, S3/OAuth client construction, timers, DB connections) and fast.

The trade-off: the `routes` (and `static get middleware()` / `getHttpPath()`) getter must return a **plain object literal** that the parser can read directly. It may reference handler methods (`handler: this.postLogin`) and inline schemas, but it must not be computed — no reading constructor state, no loops, conditionals, computed keys, or `super` calls in the returned shape.

```ts
// ✅ fine — the constructor sets up clients, but `routes` returns a literal
class Files extends AbstractController {
  constructor(app, prefix) {
    super(app, prefix);
    this.s3 = new S3Client(app.getConfig('s3')); // used inside handlers, not in routes
  }
  get routes() {
    return { post: { '/upload': { handler: this.upload } } };
  }
}

// ❌ not analyzable — `routes` is computed from constructor state
class Crud extends AbstractController {
  constructor(app, prefix) {
    super(app, prefix);
    this.models = ['User', 'Order'];
  }
  get routes() {
    // a computed return — the AST can't read these route shapes
    return Object.fromEntries(this.models.map((m) => [`/${m}`, { handler: this.list }]));
  }
}
```

If a `routes` getter isn't a static literal, codegen can't analyze it and **`npm run gen` fails** (declarative-only — there is no constructor fallback). Move the dynamic part into handlers or a module-level constant so the returned shape stays literal.

#### Setup

Add to `package.json`:

```json
"gen": "node src/cli.ts generatetypes",
"check:types": "npm run gen && tsc --noEmit"
```

Add to `.gitignore`:

```
**/*.routes.gen.ts
```

The gen files are regenerated on every type-check, so they stay fresh; CI doesn't need any extra step.

#### Naming convention

Handler method `postLogin` → type `PostLoginRequest`. Method `verifyUser` → `VerifyUserRequest`. The convention is method name in PascalCase + `Request` suffix. Renames flow naturally with editor refactor tools.

If the same handler method serves multiple routes (e.g., a backward-compatible POST and GET sharing one method), the type is a union of the per-route shapes — narrow with `req.method` inside the handler.

#### Routes without schemas

Bare-method-ref routes like `'/logout': this.postLogout` (no `request:` field) get a type that omits the schema-output override; `req.appInfo.request` falls through to the default `Record<string, unknown>` from `BaseAppInfo`.

#### Middleware-provided types

To make a middleware contribute typed fields to `req.appInfo`, add a `static get provides()` getter:

```ts
class GetUserByToken extends AbstractMiddleware {
  static get provides() {
    return {} as { user?: InstanceType<TUser> };
  }

  async middleware(req, res, next) {
    // ... runtime logic ...
  }
}
```

The returned object is always `{}` — only the cast type matters. Codegen reads this; the runtime ignores it. Handlers downstream of this middleware (per the route's middleware chain) get `req.appInfo.user` typed.

The same pattern works for **project-side middlewares** that augment `req.appInfo`. Example — a `UserWithRole` middleware that resolves CASL permissions onto the request:

```ts
import type { MongoAbility } from "@casl/ability";
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class UserWithRole extends AbstractMiddleware {
  static get provides() {
    return {} as { permissions: MongoAbility };
  }

  async middleware(req, res, next) {
    req.appInfo.permissions = await buildPermissions(req.appInfo.user);
    next();
  }
}
```

Any route with `UserWithRole` in its middleware chain now has `req.appInfo.permissions` typed as `MongoAbility` automatically.

For app-wide globals (e.g., `requestId`, `sentryTransaction`) that aren't tied to a specific middleware, augment `AppInfoExtensions`:

```ts
declare module "@adaptivestone/framework/services/http/types" {
  interface AppInfoExtensions {
    requestId: string;
  }
}
```

#### Extending the framework's User model

If your project's `User` model adds methods or fields beyond the framework's base (`getSuppliers()`, custom relations, etc.), `req.appInfo.user` from `GetUserByToken.provides` will still be typed as the framework's `TUser` — calls to project-specific methods won't compile.

Augment the framework's `User` module to add your project's fields:

```ts
// somewhere in your project, loaded before handlers compile
declare module "@adaptivestone/framework/models/User" {
  interface IUserMethods {
    getSuppliers(): Promise<string[]>;
  }
}
```

Now `req.appInfo.user.getSuppliers()` type-checks across all handlers.

#### Manual fallback (without codegen)

If you'd rather not run codegen, you can pull a typed shape from any Standard Schema validator with `StandardSchemaV1.InferOutput`:

```ts
import type { StandardSchemaV1 } from "@adaptivestone/framework/services/validate/types";
import { object, string } from "yup";

const loginSchema = object({
  email: string().email().required(),
  password: string().required(),
});

type LoginRequest = StandardSchemaV1.InferOutput<typeof loginSchema>;

async postLogin(
  req: FrameworkRequest & { appInfo: { request: LoginRequest } },
  res: Response,
) {
  // req.appInfo.request.email is typed
}
```

This works for Zod, Valibot, ArkType too. Trade-off: middleware-contributed fields (`req.appInfo.user` and friends) need to be intersected by hand on every handler, and renames don't propagate.

### File Validation

Files are uploaded via `multipart/form-data`. The parser preserves the raw form shape, which means **every multipart field arrives as an array** — a single value is a one-element array, several values are a longer array. Validate against the framework-exported `File` type, using your validator's own array support to declare cardinality:

```ts
import { File } from "@adaptivestone/framework/types.js";
import { z } from "zod";

request: z.object({
  // one file: validate the one-element array, unwrap to a File for the handler
  avatar: z.array(z.instanceof(File)).length(1).transform(([f]) => f),
  // many files (`<input type="file" multiple>`): keep the array
  avatars: z.array(z.instanceof(File)).nonempty(),
});
```

`req.appInfo.request.avatar` is a `File`; `req.appInfo.request.avatars` is `File[]`.

:::caution Cardinality is declared, not inferred from length
A single value and a one-element array look identical on the wire (`["A"]`), so you must **declare** which fields are scalar — never auto-unwrap by array length. A `multiple` field that receives one file still arrives as `["A"]`; because `avatars` is declared `z.array(...)` (no transform), it correctly stays a one-element `File[]`. Only fields you explicitly transform to a scalar (`.length(1).transform(...)`) are unwrapped — and a scalar field that receives two values fails `.length(1)`, which is the right outcome.
:::

`File` is exported as both a value (so `instanceof` works) and a type. It aliases the parser's file class today and re-points at the web-standard `File` after the transport-neutral parser swap — so your validation code stays stable across that change. The same `File` works with every validator: `z.instanceof(File)`, `v.instance(File)`, `type.instanceOf(File)`, or yup's `mixed().test("file", "not a file", (v) => v instanceof File)`.

#### How route types are generated for multipart

Route-type generation is **parser-agnostic** — the multipart always-array shape never reaches it. Codegen emits `req.appInfo.request: StandardSchemaV1.InferOutput<...["request"]>`, which reads the request schema's **output** type. Because the schema above transforms the one-element array down to a `File`, the generated handler type is the unwrapped shape (`{ avatar: File; avatars: File[] }`) — and it matches the runtime value. The schema is the single source of truth for both runtime and types.

:::info Planned — route-level single-element extraction
A **route-level option** is planned so a route can declare which multipart fields are scalar, and the router unwraps their single-element arrays *before* validation. The schema then stays the clean logical shape (`avatar: z.instanceof(File)`), codegen reads that output type directly, and there's no per-field `.array().length(1).transform(...)` to write. This keeps the array-handling in the parser layer (where the array-ness originates) instead of the schema. Until it ships, use the validator's array handling shown above.
:::

:::warning Deprecated: `YupFile`
The yup-specific `YupFile` helper (`@adaptivestone/framework/helpers/yup.js`) is **deprecated and will be removed in v6** — migrate to the `File` export above. It still works for now (and requires `yup` in your own dependencies, since the framework no longer bundles it):

```js
import { YupFile } from "@adaptivestone/framework/helpers/yup.js";

request: yup.object().shape({
  someFileName: new YupFile().required("error text"),
});
```
:::

:::warning
Please be aware that a file can only be uploaded by ‘multipart/form-data’, and because of this, you can’t use nested objects.
:::

### Different schemas per Content-Type

A single route can accept more than one `Content-Type` with a different body shape for each — for example, create a resource from JSON or upload it as `multipart/form-data`. Instead of one `request` schema, pass a **content-type map** (the shape mirrors OpenAPI's `requestBody.content`):

```ts
import { File } from "@adaptivestone/framework/types.js";
import { z } from "zod";

"/avatar": {
  handler: this.setAvatar,
  request: {
    "application/json": z.object({ url: z.string().url() }),
    "multipart/form-data": z.object({
      file: z.array(z.instanceof(File)).length(1).transform(([f]) => f),
    }),
  },
},
```

The body is parsed (the parser is already Content-Type-aware), then validated with the schema matching the request's `Content-Type`. An unmatched type returns **415 Unsupported Media Type**, listing the accepted ones.

`req.appInfo.request` becomes a **discriminated union** keyed by an injected `contentType` field, so the handler narrows cleanly — and codegen emits the union for you:

```ts
async setAvatar(req: SetAvatarRequest, res: Response) {
  if (req.appInfo.request.contentType === "multipart/form-data") {
    req.appInfo.request.file; // File
  } else {
    req.appInfo.request.url; // string
  }
}
```

:::note
Matching is on the media type only and is **case-insensitive** — parameters like `; charset=...` and `; boundary=...` are ignored. The injected `contentType` is the lower-cased media type, and it overwrites any body field of the same name, so don't declare a schema field named `contentType`. A `Content-Type` the body parser itself can't handle (e.g. malformed `multipart/form-data`) is rejected with a `400` by the parser *before* the `415` check. Middleware-declared request schemas (`relatedRequestParameters`) still apply on top, regardless of Content-Type.
:::

### Custom validators

To plug in a validator that doesn't already implement Standard Schema (e.g., raw [Joi](https://joi.dev/), or a hand-rolled function), implement the `~standard` slot directly. About 10 lines of glue:

```ts
import type { StandardSchemaV1 } from "@adaptivestone/framework/services/validate/types";

interface ProductInput { sku: string; price: number }

const productSchema: StandardSchemaV1<unknown, ProductInput> = {
  "~standard": {
    version: 1,
    vendor: "mycustom",
    validate(value) {
      const data = value as Partial<ProductInput>;
      if (typeof data.sku !== "string") {
        return { issues: [{ message: "sku is required", path: ["sku"] }] };
      }
      if (typeof data.price !== "number") {
        return { issues: [{ message: "price is required", path: ["price"] }] };
      }
      return { value: { sku: data.sku, price: data.price } };
    },
  },
};

// Use it on a route:
request: productSchema;
```

You also get `InferOutput<typeof productSchema>` for free. Standard Schema's spec lives at https://standardschema.dev/.

### Registering vendor drivers

For library-specific behavior (custom strip semantics, native JSON Schema export for OpenAPI, etc.), register a `ValidatorDriver`:

```ts
import { ValidateService, type ValidatorDriver } from "@adaptivestone/framework/services/validate/ValidateService";

const myJoiDriver: ValidatorDriver = {
  canHandle: (body) => body?.isJoi === true,
  async validate(body, data) {
    const { value, error } = body.validate(data, { stripUnknown: true });
    if (error) throw new ValidationError(joiToFrameworkPayload(error));
    return value;
  },
  toJsonSchema: (body) => myJoiToJsonSchema(body), // optional — supplies the OpenAPI body schema
};

ValidateService.register(myJoiDriver);
```

Drivers are matched in registration order; user-registered drivers take priority over the built-ins.

### ValidationError

When validation fails, the framework throws a `ValidationError`. The instance's `.message` is the path-keyed payload object that ships out via `res.json({ errors: err.message })`, producing:

```json
{
  "errors": {
    "fieldName": ["error description"],
    "anotherField": ["another field error"]
  }
}
```

Each value is always an array of messages. A field that fails multiple validators surfaces all of them: `{password: ["min8", "startUpper"]}`.

Validation errors cover input that fails the declared schema **before** the handler runs. For errors thrown **inside** the handler — typed HTTP errors like `NotFoundError`, third-party errors you map yourself, escaped Mongoose validation — see [Error handling](04-error-handling.md).

For structured access (logging, observability), use `.issues`:

```ts
import { ValidationError } from "@adaptivestone/framework/services/validate/ValidationError";

try {
  /* ... */
} catch (e) {
  if (e instanceof ValidationError) {
    for (const issue of e.issues) {
      console.error(`[${issue.path?.join(".") ?? "root"}] ${issue.message}`);
    }
  }
}
```

### i18n

In any fields that can generate an error (required, etc.), you can use i18n keys to translate. The framework will handle the translation for you.

Please refer to the [i18n documentation](08-i18n.md).

## Handler

Handler - some async function (most likely in this controller file) that will do all the work. It is better to write the function in the same file.

:::warning
The handler can only be an **async** function.
:::

`req.appInfo.app`

```js
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      post: {
        '/': {
          handler: this.postSample,
          request: yup.object().shape({
            count: yup.number().max(100).required(),
          })
        }
      }
    }
  }
  // Send a request with data {count: "5000"}.
  // Will produce an error with status 400 and {errors: {count:['Text error']}}.

  postSample(req, res) {
    // On success validation, we pass here.
    // {count: "5000"}
    console.log(req.appInfo.request)
    // {count: 5000} -> casted to a number

    const SomeModel = this.app.getModel('SomeModel');
    const SomeModelAlternativeWay = req.appInfo.app.getModel('SomeModel');

    const { count } = req.appInfo.request;

    const someModel = await SomeModel.findOne({count});

    return res.status(200).json({modelId: someModel.id});
  }

}
export default ControllerName;
```

### Middleware

Middleware - an array of middlewares specially for the current route.

:::warning

Route middlewares take precedence over middlewares in controllers.

:::

```javascript
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      get: {
        '/routeName': {
          handler: ...,
          middleware: [MiddlewareName, MiddlewareName, etc]
        }
      },
    };
  }
}
export default ControllerName;
```

Similarly to controller middlewares, you can use middlewares with parameters.

:::note

The rules for the design of middlewares with parameters are described in the subsection "Middleware".

:::

Sample:

```javascript
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
import RoleMiddleware from "@adaptivestone/framework/services/http/middleware/Role.js";

class ControllerName extends AbstractController {
  get routes() {
    return {
      get: {
        '/routeName': {
          handler: ...,
          middleware: [[RoleMiddleware, { roles: ['client'] }]]
        }
      },
    };
  }
}
export default ControllerName;
```

## Debugging your routes

### Boot-time route tree

After all controllers are registered, the framework prints the full route tree at the `verbose` log level — useful for spotting cross-controller middleware accumulation, splat scopes, or unexpected route shapes. Set `LOGGER_CONSOLE_LEVEL=verbose` (or your transport's equivalent) to see it.

```
Registered routes:
/  (mw: GetUserByToken)
├── GET     /
├── auth  (mw: RateLimiter; pmw: GetUserByToken)
│   ├── login
│   │   └── POST    /auth/login  [request]
│   └── logout
│       └── POST    /auth/logout
└── v1  (mw: ApiLimiter; pmw: GetUserByToken)
    └── container
        ├── GET     /v1/container  [query]
        └── POST    /v1/container  [query]

5 route(s) across 6 node(s) in the tree.
```

Each method line is `METHOD` (padded) followed by the route's full path — handler names aren't printed. `[request]` / `[query]` markers indicate routes with body / query schemas. For middleware: `(mw: …)` lists what's newly attached at that node or route, `pmw: …` lists the inherited chain that already runs above it (deduped, in run order), and `{…}` after a middleware name means it was registered with parameters.

### Warnings on misconfiguration

The framework logs a `warn`-level message and skips the offending entry when it sees these problems in your `routes` getter or middleware `Map`:

| Warning | Triggered when |
|---|---|
| `unknown verb 'X' in routes getter` | A key in `get routes()` isn't one of `get/post/put/patch/delete/head/options` |
| `route X Y has no handler function` | A route's value is an object but its `handler` field is missing or not callable |
| `middleware Map key is not a string` | A key in `static get middleware()`'s `Map` is not a string |
| `middleware Map key 'X' has unknown method prefix 'Y'` | A `Map` key looks like `METHOD/path` but `METHOD` is not a known HTTP verb — the whole key is treated as a path, which is usually a typo |

These warnings catch the common typos that used to silently produce 404s at request time.


---


# Document 9: 06-Controllers > 03-middleware

<!-- Source: 06-Controllers/03-middleware.md -->

# Middleware

You can read more about middlewares at [https://expressjs.com/en/guide/using-middleware.html](https://expressjs.com/en/guide/using-middleware.html).

In general, it’s a function that accepts a request, a response, and a `next` callback. This function can analyze requests and add more details to them (like parsing JSON, getting query params, or checking a user token). Middleware can pass requests to the next level (the next middleware or handler) or can respond directly and finish the request.

This is really powerful and allows developers to reuse simple logic and build routes on these simple building blocks.

Default:

```js
  static get middleware() {
    return new Map([['/{*splat}', [GetUserByToken, Auth]]]);
  }
```

## Middleware Order

Middleware will be executed in the order provided. Based on that, you can chain middleware where the input of the second middleware depends on the output of the first middleware.

## Global Middlewares

The framework internally uses a few middlewares. These middlewares are not adjustable (for now) and are executed on each request, in this order (security response headers are applied first, before any of these):

[RequestLogger](#requestlogger)

[PrepareAppInfo](#prepareappinfo)

[IpDetector](#ipdetector)

[I18n](#i18n)

[Cors](#cors)

[RequestParser](#requestparser)

## Including Middlewares into Controllers

Controller-level middleware is adjusted based on the “middleware” getter.

```js
  static get middleware() {
    return new Map([['METHOD/path', ["Middleware","Array"]]]);
    // return middlewares for THIS route only
  }
```

Where 'METHOD/path' is a method with a path. Supported methods are `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`, and the pseudo-verb `ALL` (matches all methods). If the middleware key starts with `/`, then `ALL` methods are used.

The path follows the same patterns as routes — literals, `:name` params, and `{*name}` splats. See [Routes › path syntax](02-routes.md#route-second-level-path-level) for the full reference.

The middleware array is an array of middlewares (with params).

Sample:

```javascript
  static get middleware() {
    return new Map([['GET/{*splat}', [GetUserByToken]]]);
  }
```

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        GetUserByToken,
        [RoleMiddleware, { roles: ['admin'] }]
      ]]
    ]);
  }
```

:::warning
The middleware here are not raw Express middlewares. Please see below.
:::

## Including Middlewares into a Route Object

Middlewares can also be added into a route object (subchapter “Routes”).

## Middleware Parameters

Some middleware accept initial parameters passed into them.

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        GetUserByToken, // middleware with no parameters
        [RoleMiddleware, { roles: ['admin'] }] // middleware with parameters
      ]]
    ]);
  }
```

To pass parameters, wrap the middleware in an array. The first element will be the middleware itself, and the second one will be the middleware parameters. The second one will be passed as is into the middleware constructor.

## Built-in Middleware

The framework has a few middlewares that you can use.

### Auth

```js
import Auth from "@adaptivestone/framework/services/http/middleware/Auth.js";
```

Allows passing only if the user is provided. Please use any middleware that provides a user instance beforehand (like `GetUserByToken`).

#### Parameters

No parameters.

### Cors

```js
import Cors from "@adaptivestone/framework/services/http/middleware/Cors.js";
```

Adds CORS headers if the origin matches the config.

#### Parameters

`origins` - an array of strings or regex to check the origin. Required parameter.

```javascript
  static get middleware() {
    return new Map([
      ['GET/someUrl', [
        [Cors, { origins: ['http://localhost',/./] }]
      ]]
    ]);
  }
```

### GetUserByToken

```js
import GetUserByToken from "@adaptivestone/framework/services/http/middleware/GetUserByToken.js";
```

Grabs a token and tries to parse the user from it. It will find the user in the database by the token. If the user exists, it will add the `req.appInfo.user` variable.

#### Parameters

No parameters.

### I18n

```js
import I18n from "@adaptivestone/framework/services/http/middleware/I18n.js";
```

An internationalization module based on [i18next](https://www.npmjs.com/package/i18next). It provides `req.appInfo.i18n` that can be used for translation.

The middleware provides a few detectors:

- X-Lang header
- Query
- User

Please check the [i18n documentation](08-i18n.md) for more details.

#### Parameters

No parameters.

### IpDetector

```js
import IpDetector from "@adaptivestone/framework/services/http/middleware/IpDetector.js";
```

This middleware will detect the client's IP. It works well with different proxies (AWS ELB, Nginx, etc.) and detects the real client IP.

:::note
If the request IP is from a `trustedProxy` (trusted source) only, then the module will try to parse the IP from the provided `X-Forwarded-For` header and grab the client IP from there. Otherwise, the request IP will be used.
:::
This is a core middleware, and some other middlewares (like `RateLimiter`) depend on it.

#### Parameters

All parameters go into the config file. There are two parameters there: `headers` and `trustedProxy`.

`headers` is an array of headers to parse the IP address from. By default, it is 'X-Forwarded-For'.

`trustedProxy` is an IP, CIDR, or range of IPv4 and IPv6 that is trusted to parse headers from.

```javascript
  headers: ['X-Forwarded-For'],
  trustedProxy: [ // list of trusted proxies.
    '169.254.0.0/16', // ipv4 cidr
    'fe80::/10', // ipv6 cidr
    '127.0.0.1', // ip itself
    '1.1.1.1-1.1.1.3', // ip range
  ],
```

IP data is available at:

```javascript
req.appInfo.ip;
```

:::warning
Select `trustedProxy` really carefully, as anyone can add any headers to your request.
:::

Nginx sample to add a header:

```bash
server {
    location xxxx/ {
      proxy_set_header  X-Forwarded-For $remote_addr;
    }
  }

```

### Pagination

```js
import Pagination from "@adaptivestone/framework/services/http/middleware/Pagination.js";
```

The pagination middleware provides a helper that grabs URL search parameters (`page`, `limit`) and calculates the necessary `appInfo` properties (`skip`, `limit`, and `page`).

#### Parameters

`limit` = 10 - default limit if not provided.

`maxLimit` = 100 - max limit for documents.

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        [Pagination, { limit: 10,maxLimit: 50}]
      ]]
    ]);
  }
```

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [Pagination]
    ]);
  }
```

```javascript
// http://localhost:3300/someUrl?limit=10&page=2
const { limit, skip, page } = req.appInfo.pagination;
```

### PrepareAppInfo

```js
import PrepareAppInfo from "@adaptivestone/framework/services/http/middleware/PrepareAppInfo.js";
```

`PrepareAppInfo` is a special small middleware that generates `req.appInfo = { app }`. This is to make sure that all subsequent middleware can use `appInfo` without checking if it exists.
It is for internal use.

#### Parameters

No parameters.

### RateLimiter

A rate limiter middleware. Limits the amount of requests.

```js
import RateLimiter from "@adaptivestone/framework/services/http/middleware/RateLimiter.js";
```

For rate limiting, we are using the [node-rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) module. Please refer to the module documentation for more details.

The basic idea of a rate limiter is that we have some weight for the call and some key that has ‘credits’. Each call consumes ‘credits’, and when it reaches 0, the request is blocked.

Some samples - login protection. We can generate rate limiters based on the user's email and limit each email to only 5 calls per minute. Or we can construct a more complex login that includes user IPs, etc.

#### Parameters

By default, the rate key is generated based on the Route, IP, and userID. But you can adjust it via the config (globally) or via middleware parameters.

```javascript
  static get middleware() {
    return new Map([
      [
        'POST/login',
        [
          GetUserByToken,
          [
            RateLimiter,
            {
              consumeKeyComponents: { ip: false },
              limiterOptions: { points: 5 },
            },
          ],
        ],
      ],
    ]);
  }
```

The rate limiter middleware allows you to include request components (`req.body`) for key generation. Please note that you have no access to `req.appInfo.request` at this stage.

```javascript
  static get middleware() {
    return new Map([
      ['POST/login', [
        GetUserByToken,
        [RateLimiter,{consumeKeyComponents: { ip: false, request:['email','phone'] }}]
      ]]
    ]);
  }
```

You can find the default parameters in ‘config/rateLimiter.js’. These parameters are used if other parameters are not provided.

The rate limiter has multiple backends, selected via the `driver` option (`'mongo'`, `'memory'`, or `'redis'`). The default is `'mongo'`. The `'redis'` backend lazy-loads `@redis/client`, which is an **optional peer dependency** — install it (`npm i @redis/client`) when you choose that driver; the `'memory'` and `'mongo'` backends never load it.

### RequestLogger

```js
import RequestLogger from "@adaptivestone/framework/services/http/middleware/RequestLogger.js";
```

A small middleware that logs request info (route, method, status, and time).

Logs example:

```js
[middlewareRequestLogger]  2023-01-24T07:00:35.680Z  info : Request is  [GET] /project/123
[middlewareRequestLogger]  2023-01-24T07:00:35.747Z  info : Finished Request is  [GET] /project/123.  Status: 200. Duration 67 ms
```

#### Parameters

No parameters.

### RequestParser

```js
import RequestParser from "@adaptivestone/framework/services/http/middleware/RequestParser.js";
```

This is the main middleware to parse requests (`application/json`, `multipart/form-data`, `application/octet-stream`, `application/x-www-form-urlencoded`).
It is based on the [formidable](https://www.npmjs.com/package/formidable) package.
After parsing, the data is available in `req.body`.

#### Parameters

No parameters.

### Role

Checks the user role (`user.roles` property). If the user does not have the role, it stops the request and returns an error. It uses OR logic (any of the specified roles will allow the user to pass).

```js
import Role from "@adaptivestone/framework/services/http/middleware/Role.js";
```

#### Parameters

`roles` - an array of roles to check. It uses OR logic (any role will pass).

```javascript
  static get middleware() {
    return new Map([
      ['POST/someUrl', [
        [RoleMiddleware, { roles: ['admin','moderator'] }]
      ]]
    ]);
  }
```

### StaticFiles

:::warning

Deprecated and removed in version 5. It is better to use an HTTP server (Nginx, etc.) to handle static files.
:::

```bash
# nginx sample
server {

	root /var/www/application/src/public;

	server_name _;
	client_max_body_size 64M;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ @backend;
	}

	location @backend {
		proxy_pass http://localhost:3300;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_cache_bypass $http_upgrade;
	}
}

```

## Creating Your Own Middlewares (or Integrating External Ones)

You can create your own middleware. To do that, you should extend `AbstractMiddleware` and provide at least two of your own functions: `description` and `middleware`. Please check the code below.

```js
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class CustomMiddleware extends AbstractMiddleware {
  static get description() {
    return "Middleware description";
  }

  // optional
  static get usedAuthParameters() {
    // Security scheme(s) this middleware enforces — read by the OpenAPI
    // generator into `components.securitySchemes` (see the OpenAPI chapter).
    return [
      {
        name: "Authorization", // name of the parameter
        type: "apiKey", // apiKey, http, oauth2, openIdConnect
        in: "header", // header, query, cookie
        description: this?.description,
      },
    ];
  }

  // optional — declared `static` so the framework reads the schema WITHOUT
  // instantiating the middleware (no constructor side effects at route setup /
  // codegen)
  static get relatedQueryParameters() {
    // A Standard Schema-conformant schema (Zod, Valibot, ArkType, yup ≥1.7, or
    // `defineSchema`) for middleware-related `req.query` parameters. Validated
    // and exposed on `req.appInfo.query`, relative to the route the middleware
    // is declared on.
    return yup.object().shape({
      limit: yup.number(), // For example
    });
  }

  // optional
  static get relatedRequestParameters() {
    // Same, for middleware-related `req.body` parameters — exposed on
    // `req.appInfo.request`.
    return yup.object().shape({
      name: yup.string().required(), // For example
    });
  }

  async middleware(req, res, next) {
    // check something
    if (!req.body.yyyyy) {
      //  return and stop processing
      return res.status(400).json({});
    }
    if (this.params.iiii) {
      // we can also check all the params that we passed during init
    }
    // go to the next one
    return next();
  }
}

export default CustomMiddleware;
```

`static get usedAuthParameters()` declares the security scheme(s) the middleware enforces. The OpenAPI generator reads it off the class (no instantiation) to populate `components.securitySchemes` and mark every route in the middleware's chain as secured — see [OpenAPI › Documenting auth](../17-openapi.md#documenting-auth-security-schemes) for the field reference and an `http`/`bearer` example.

:::warning Deprecated: instance schema getters
The non-static form — `get relatedQueryParameters()` / `get relatedRequestParameters()` (and `get relatedReqParameters()`) — is **deprecated and will be removed in v6**. It forces the framework to instantiate the middleware (running its constructor) just to read the schema, so use `static get` instead. The instance form still works through v5: when it's detected, the framework instantiates the middleware as a fallback and emits a one-per-class `DeprecationWarning` (`ASF_DEP_MW_INSTANCE_SCHEMA`).
:::

### Typed contributions to `req.appInfo`

If your middleware sets fields on `req.appInfo`, declare them via `static get provides()` so codegen can type them on every downstream handler — no per-handler intersections required.

```ts
import type { MongoAbility } from "@casl/ability";
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class UserWithRole extends AbstractMiddleware {
  static get provides() {
    // Phantom — the runtime returns this `{}` but only the cast matters.
    return {} as { permissions: MongoAbility };
  }

  async middleware(req, res, next) {
    req.appInfo.permissions = await buildPermissions(req.appInfo.user);
    next();
  }
}
```

Any route with `UserWithRole` in its chain now has `req.appInfo.permissions` typed automatically. The cast type is the contract; the runtime value is ignored.

See the [routes chapter codegen section](./02-routes.md#middleware-provided-types) for the full picture of how generated request types compose middleware-provided fields, schemas, and path params.



---


# Document 10: 06-Controllers > 04-error-handling

<!-- Source: 06-Controllers/04-error-handling.md -->

# Error handling

What happens when a route handler throws? The framework resolves the error through an ordered **error-handler registry**:

1. **Your registered handlers** — checked first, in registration order.
2. **Built-ins** — the `HttpError` mapper, then the Mongoose validation safety net.
3. **Fallback** — nothing matched: the error is logged at `error` level and the client gets `500 {"message": "Platform error. Please check later or contact support"}`.

The first entry whose error class matches (`instanceof`) and whose handler returns a response wins. That gives you two tools: **throw** typed HTTP errors from your own code, and **register** handlers for error types you don't own.

## Throwing HTTP errors from your code

Deep inside business logic you often know the right HTTP answer — the document doesn't exist, the user isn't allowed — but you don't have `res` there, and threading it through every function is noise. Throw instead:

```js
import { NotFoundError, ForbiddenError } from "@adaptivestone/framework/services/http/httpErrors.js";

class Posts extends AbstractController {
  get routes() {
    return {
      get: { "/:id": { handler: this.getOne } },
    };
  }

  async getOne(req, res) {
    const post = await this.app.getModel("Post").findById(req.params.id);
    if (!post) {
      throw new NotFoundError("Post not found");
      // → 404 {"message": "Post not found"}
    }
    if (String(post.ownerId) !== req.appInfo.user.id) {
      throw new ForbiddenError("Not your post");
      // → 403 {"message": "Not your post"}
    }
    return res.status(200).json({ data: post.getPublic() });
  }
}
```

It works from any depth — a service function three calls down can throw the same way.

Available classes (all from `services/http/httpErrors.js`):

| Class | Status | Default message |
| --- | --- | --- |
| `BadRequestError` | 400 | `Bad request` |
| `UnauthorizedError` | 401 | `Unauthorized` |
| `ForbiddenError` | 403 | `Forbidden` |
| `NotFoundError` | 404 | `Not found` |
| `ConflictError` | 409 | `Conflict` |
| `HttpError` | any | — (base class) |

Every constructor accepts `(message, body?)`. The response body is `{ message }` unless you pass an explicit `body`, which replaces it:

```js
throw new HttpError(422, "Unprocessable", { errors: { csv: "row 17 malformed" } });
// → 422 {"errors": {"csv": "row 17 malformed"}}
```

For a status you use often, subclass once and throw everywhere:

```js
import { HttpError } from "@adaptivestone/framework/services/http/httpErrors.js";

export class PaymentRequiredError extends HttpError {
  constructor(message = "Subscription expired") {
    super(402, message);
  }
}
```

Thrown `HttpError`s are logged at `verbose` level — they're deliberate control flow, not defects, so they don't pollute your error logs.

## Mapping errors you don't own

Libraries throw their own error types — the Mongo driver, payment SDKs, queue clients. Register a handler for the class, typically from the [`bootHttp` hook](01-intro.md#project-boot-hook-boothttp) (the `Server` constructor option that runs with the live app):

```js
import { MongoServerError } from "mongodb";

const server = new Server({
  ...folderConfig,
  bootHttp: async (app) => {
    app.httpServer?.registerErrorHandler(MongoServerError, (err) =>
      err.code === 11000
        ? { status: 409, body: { message: "Already exists" } }
        : null, // null = "not mine after all" → try the next entry
    );
  },
});
```

The handler contract:

- **Signature**: `(err, req) => { status, body } | null` — async is fine, the result is awaited.
- `err` is typed as an instance of the class you registered — `err.code` autocompletes, no casts.
- `req` is the same request the route handler had — `req.appInfo.request`, `req.appInfo.i18n`, etc.
- Return `{ status, body }` to produce the response (the framework sends it — handlers never touch `res`, so double-send protection and logging stay in one place).
- Return `null`/`undefined` to pass the error to the next entry.
- `registerErrorHandler` returns an **unregister function** — handy in tests.
- Third argument `{ logLevel }` controls how the handled error is logged (default `warn`):

```js
app.httpServer?.registerErrorHandler(
  StripeCardError,
  (err) => ({ status: 402, body: { message: err.declineReason } }),
  { logLevel: "verbose" },
);
```

If a handler itself throws, the framework logs it (with the stack) and falls back to the 500 — a broken error handler can never crash the request pipeline.

Handler-side types, if you want to extract the function:

```ts
import type { ErrorHandlerFn, ErrorHandlerResult } from "@adaptivestone/framework/services/http/builtinErrorHandlers.js";
```

### Reading the request in a handler

`req` is the full framework request, so a handler can build responses from everything the route knew — path params, validated body and query, locale, client IP. A fuller example: a task-creation route hits a unique index, and the handler turns the raw driver error into an answer that names what collided and where:

```js
// Route: POST /project/:projectId/tasks?notify=email
//        request: object({ title: string().required() })
//        query:   object({ notify: string() })
// Model: title has a unique index per project → E11000 on duplicates.

app.httpServer?.registerErrorHandler(MongoServerError, (err, req) => {
  if (err.code !== 11000) {
    return null; // other driver errors → next entry (→ 500 fallback)
  }

  // Which unique field collided — E11000 carries it in `keyValue`.
  const [field] = Object.keys(err.keyValue ?? {});

  return {
    status: 409,
    body: {
      // Translated for the request's locale, like any handler would do.
      message: req.appInfo.i18n?.t("errors.taskExists") ?? "Task already exists",
      field,                                    // "title"
      projectId: req.params.projectId,          // raw path param (string)
      attempted: req.appInfo.request?.title,    // validated body value
      notify: req.appInfo.query?.notify ?? null, // validated query value
    },
  };
});
```

What's available on `req`:

| Source | What it is | Caveat |
| --- | --- | --- |
| `req.params` | Path params (`:projectId`) | Raw **strings**, not validated — same rule as in handlers (see the note in [Routes → Validation](02-routes.md#validation)) |
| `req.appInfo.request` | Validated, cast request body | Only set when the route declares a `request:` schema — guard with `?.` in handlers registered for many routes |
| `req.appInfo.query` | Validated, cast query string | Same — needs a `query:` schema |
| `req.appInfo.i18n` | `t()` + detected `language` | Present on framework routes; typed optional |
| `req.appInfo.ip` | Client IP (proxy-aware) | From the global IP detector |
| `req.appInfo.user` | Authenticated user document | Only on routes running the auth middleware (`GetUserByToken`) |
| `req.method`, `req.path`, `req.headers`, … | Anything Express exposes | — |

One design boundary to keep in mind: the handler decides the **response**; the framework does the sending and the logging. If you find a handler reaching for `res` or a logger, that logic probably belongs in the route handler's own `try/catch` instead.

## Matching order

Entries are matched by `instanceof` in **registration order — not by class specificity**. Your handlers always run before the built-ins, so you can intercept or override anything, including the built-ins themselves.

:::warning
An early handler for a base class shadows later handlers for its subclasses. If you register a handler for `HttpError` and later one for `NotFoundError`, the `HttpError` one wins for every `NotFoundError` thrown — it was registered first and `NotFoundError instanceof HttpError` is true. Register the specific classes first, or branch inside one handler.
:::

## Built-in: the Mongoose validation safety net

The recommended practice is to mirror model constraints in your route schema — a `maxLength: 50` in the model should have a `.max(50)` in the route's `request:` schema, so bad input fails fast with a clean, translated 400 (see [Validation](02-routes.md#validation)).

But when a constraint slips through, `doc.save()` throws a Mongoose `ValidationError`, and a built-in registry entry catches it:

- If **every** failing model path is a field the client actually sent (a key of the validated `request:`/`query:` input), the client gets `400 {"errors": {"name": "..."}}` — same shape as route validation errors — and the framework logs a `warn`: your route schema is missing a constraint worth mirroring.
- If **any** failing path is internal or renamed (the client sent `name`, the model field is `userName`), it stays an honest **500**. Model field names are never leaked to the client, and a server-side data bug is never blamed on the client.

Each message is **rebuilt from the validation kind and the schema constraint** — `maxlength` → `"Must be at most 255 characters"`, a `Number` cast failure → `"Must be a number"`, `enum` → `"Must be one of: …"` — and **never includes the value the client submitted**. Mongoose's own default messages interpolate that value (a phone number, a password pasted into the wrong field), which would otherwise leak it into the response and the log. For the same reason a *custom* message set on the model (`maxLength: [50, 'Name too long']`) is **not** passed through — it's rebuilt generically, since a custom string can't be told apart from a templated default that embedded the value. The `warn` log line for a handled error is sanitized the same way; a failure that stays a 500 logs the original error in full. These fallback messages are plain English and not translated; put user-facing, i18n wording on the route schema.

Note this covers Mongoose *validation* errors only. A duplicate-key violation (`E11000`) is a `MongoServerError` from the driver, not a `ValidationError` — map it yourself as shown above if you want a 409.

:::tip
The safety net is a fallback, not the contract. Route schemas are the API's source of truth: they produce field-accurate, i18n-translated errors under the names the client knows. The safety net exists so a missed constraint degrades to a useful 400 instead of a mystery 500.
:::

## What still becomes a 500

- Any error no registry entry claims (including `null` returns all the way down).
- A registry handler that throws while handling.
- Mongoose validation failures on internal/renamed fields (see above).

All of these are logged at `error` level with the original error, so the details are in your logs — the client only ever sees the generic message.


---


# Document 11: 07-logging

<!-- Source: 07-logging.md -->

# Logging

The framework uses the [Winston](https://github.com/winstonjs/winston) logger as the main subsystem for logging.

You can adjust different transports (console and Sentry are available by default) and add your own transport with your own parameters.

The framework provides a basic initialization of Winston so you can easily use it out of the box, but you still have the ability to adjust it as you want.

## Logging Levels

[RFC5424](https://datatracker.ietf.org/doc/html/rfc5424) defines logger levels. Higher levels will include messages from lower levels. In other words, if you set the ‘warn’ level, the logger will report ‘error’ as well, but will not report the ‘debug’ level.

You can read more about levels in the [Winston levels documentation](https://github.com/winstonjs/winston#logging-levels).

But in short, these are the default levels:

```js
{
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6
}
```

## API

Each class has access to the logger instance via:

```js
this.logger;
```

```js
this.logger.error("error message");
this.logger.warn("warn message");
this.logger.info("info message");
this.logger.http("http message");
this.logger.verbose("verbose message");
this.logger.debug("debug message");
this.logger.silly("silly message");
```

:::tip
Please note that `this.logger` is an instance of `winston.logger`, and you can use any methods from it.
:::

## Default Transports

By default, the framework provides two transports: **console** and **Sentry**

## Configuration

We try to keep the configuration simple and powerful - you are able to enable/disable loggers and add your own with all available options passed to the transport.

Configuration files are located in ‘config/log.ts’.

```js
export default {
  transports: [
    {
      transport: "sentry", // transport name (specail name or npm package name)
      transportOptions: {
        // options that will be passed to the transport instance
        level: process.env.LOGGER_SENTRY_LEVEL || "info",
      },
      enable: process.env.LOGGER_SENTRY_ENABLE || false, // whether the transport is enabled or not
    },
    {
      // ....
    },
    /// more transports
  ],
};
```

The config contains a “transports” array. Each transport can be included in the logger.

The transport **name** is an npm package name that the framework will require. You can use any transport from NPM.

The **transportOptions** field contains the transport options - you can pass any options here for the transport. Please refer to the transport documentation.

And finally, the **enable** field will enable/disable modules for the logger. You can check [“NODE_ENV” in the config documentation](02-configs.md#node_env) to learn more about how you can use it depending on your environment.

## Sentry transport 

For Sentry, we assume it is already configured in the project and we will reuse the existing setup. If Sentry is not present in the system, the framework will display a message indicating that this feature is only available when Sentry is configured.

## Add Your Own Transport

Adding your own transport is a simple process.

```js
npm i ${WINSTON_TRANSPORT_PACKAGE}
```

Then add it to the ‘config/log.ts’ config file.

```js
export default {
  transports: [
    {
      // .....
      // some already existing transports
    },
    {
      // ....
    },
    {
      transport: "WINSTON_TRANSPORT_PACKAGE",
      transportOptions: {
        // options that will be passed to the transport instance
        // .... your transport options
      },
      enable: true,
    },
  ],
};
```

:::tip

Feel free to use environment variables in your transport config as well. That simplifies working with multiple environments.

:::

## Console Output for Logger

Console loggers act in a customized way to provide more verbose info. The default console output is constructed as:

`(${process.pid}) ${info.label}  ${info.timestamp}  ${info.level} : ${info.message}`

Where:
“process.pid” - the PID of the Node process. Useful in a cluster environment.
“info.label” - the label of the place. More info below.
“info.timestamp” - the date of the event.
“info.level” - the log level (“error”, ”warn”, etc.).
“info.message” - the message that was passed to the logger (e.g., `this.logger.info(“this is a message”)`).

### info.label

Inside the base class, we have a method [“loggerGroup”](04-base.md#api) that is used as the first part of the info message generation. This is useful for grouping messages like “controllers”, ”models”, etc.

The framework uses the following groups: “command”, “connector”, “controller”, “model”, and “CLI\_”.

:::tip
Feel free to introduce your own groups.
:::

The second part is generated by the base class function “getConstructorName”, which by default grabs the constructor name and adds it to the info string. You can overwrite this method for your classes.

Example:

```
(15950)  [modelCoin]  2021-10-18T11:17:54.746Z  verbose : Model have no hooks
```

Here:

```
(15950) - process PID
[modelCoin] -  info message ("model" - group, "Coin" - model name)
2021-10-18T11:17:54.746Z - time
verbose - level
Model have no hooks - message
```

### Environment Variables

#### Sentry Transport

The framework's Sentry transport reuses the Sentry SDK your app already initialized — it does **not** read a DSN itself. Configure the DSN in your own `Sentry.init({ dsn: ... })` at startup (the project template uses the `LOGGER_SENTRY_DSN` env var there by convention). The framework reads only:

**LOGGER_SENTRY_LEVEL** - the log level that should go into Sentry. Default: 'info'.

**LOGGER_SENTRY_ENABLE** - enable or disable the Sentry logger. Default: 'false'.

#### Console Transport

**LOGGER_CONSOLE_LEVEL** - the log level. Default: 'silly' (includes all).

**LOGGER_CONSOLE_ENABLE** - enable or disable the console logger. Default: 'true'.


---


# Document 12: 08-i18n

<!-- Source: 08-i18n.md -->

# i18n

No modern app can avoid multi-language support. The framework supports internationalization out of the box.

All internationalization is based on the [i18next](https://www.i18next.com/) library.

### Middlewares 

The framework provides an I18n middleware that runs on each HTTP request.

## Detectors

As we are talking about languages, we need some way for the codebase to understand what language the user should use.

This feature is called detectors.

Order of detection:

- X-Lang header
- Query
- User

### X-Lang Header Detector

This detector will parse the “X-Lang” header on the request to detect the user's language. The frontend should add the user's language here (“en”, ”fr”, etc.), and if the backend supports it, the app will use the given user language.

:::tip
“xLang” is the preferred way to work with languages in the framework.
:::

Example for the frontend with Fetch:

```js
fetch("https://example.com/getSomething", {
  headers: {
    "X-Lang": "en", // Added language
  },
});
```
### Query Detector

The query is a simple detector. Just add the 'lookupQuerystring' parameter to your query string. 'lookupQuerystring' by default is **lng**, but you can change it as you want inside the config file.

```js
const res = await fetch("https://someUrl.com?lng=en");
```

### User Detector

The user detector tries to find an authorized user (provided by middleware) and grab the ‘locale’ field from this user.

### Adding Your Own Detector

At this time, you are not able to add your own detector. Please contact us if you need that option, and we will be happy to help you.

## Configuration

Please look at the ‘config/i18n.ts’ file for all configuration options. There are a limited number of options available.

## Language Files

All files in JSON format are located in ‘src/locales/\{localeCode\}/translation.json’.

:::note

Backends besides files are not supported.

:::

You can find detailed documentation about JSON files in the [i18next JSON documentation](https://www.i18next.com/misc/json-format).

## API

The framework provides easy integration for controllers. You can grab the i18n instance with:

```js
req.appInfo.i18n;
req.appInfo.i18n.language; // current language
req.appInfo.i18n.t("some.phrase"); // translate some phrase https://www.i18next.com/overview/api#t
```

:::tip

You can pass controller `request` / `query` error messages as i18n keys (or plain strings). The framework processes them with i18next before sending the response, regardless of which validator library produced the error — Yup, Zod, Valibot, ArkType, or a custom Standard Schema validator.

```js
class SomeController extends AbstractController {
  get routes() {
    return {
      post: {
        "/login": {
          handler: this.postLogin,
          request: yup.object().shape({
            email: yup.string().email().required("auth.emailProvided"), // <-- look here i18n
          }),
        },
      },
    };
  }
}
```

### Interpolation

Validators that produce parameters (yup's `min` / `max` / `length`, etc.) forward those parameters to i18next, so locale strings can use `{{placeholder}}` syntax:

```js
// schema
request: yup.object().shape({
  password: yup.string().min(8, "auth.passwordTooShort").required(),
});

// locales/en/translation.json
// "passwordTooShort": "Password must be at least {{min}} characters"

// response when password is too short:
// { "errors": { "password": ["Password must be at least 8 characters"] } }
```

:::


## Direct usage. 

Sometimes you may want to use i18n outside of HTTP requests (such as for emails, WebSockets, etc.). For that purpose, frameworks provide an easy way to interact with i18n using the same configuration (translations, languages, etc.).

App instance provides lang services for you

```ts
import { appInstance } from '@adaptivestone/framework/helpers/appInstance.js';

const i18nService = await appInstance.getI18nService();
const i18n = await i18nService.getI18nForLang(lang);
```

and this is preconfigured i18next instance for you.

### Validation outside HTTP

`ValidateService.validate` accepts an optional `i18n` argument. Pass it to get the same auto-translation the HTTP path provides; omit it to receive raw keys (useful in workers / RPC where you'd rather forward structured errors than translated text).

```ts
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
import ValidateService from "@adaptivestone/framework/services/validate/ValidateService.js";
import { ValidationError } from "@adaptivestone/framework/services/validate/ValidationError.js";

async function processQueueMessage(payload, schema) {
  const i18nService = await appInstance.getI18nService();
  const i18n = await i18nService.getI18nForLang("en");

  try {
    return await new ValidateService(appInstance, schema).validate(payload, i18n);
  } catch (err) {
    if (err instanceof ValidationError) {
      // err.message → translated wire-shape; err.issues → structured
      logger.error({ issues: err.issues }, "queue payload invalid");
    }
    throw err;
  }
}
```


---


# Document 13: 09-testsing

<!-- Source: 09-testsing.md -->

# Testing

The framework comes with [Vitest](https://vitest.dev/) support. When you name files with the `.test.(js|ts)` extension, they will be added to the tests. The test lifecycle is runner-agnostic, so you can also drive it from Node's built-in [`node:test`](https://nodejs.org/api/test.html) — see [Using with `node:test`](#using-with-nodetest).

:::tip
Please put test files near the main files that you are testing and give them the same name.
If you want to test “Auth.js”, please create a file named “Auth.test.js” and put it in the same folder.

:::

## Framework (app) Instance

Of course, inside a test, you need to have access to the framework instance. It will be available via appInstanceHelper

```js
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
```

## Run Tests

```bash
npm test
```

## Before Scripts

The test entry point is at the project level in ‘src/tests/setup.ts’. This file prepares all folder configs, requires the framework setup, and prepares the global setup for tests.

The minimum Vite config file should contain:

```js
  test: {
    globalSetup: [ // This script will start Mongo (DIST in important there)
      'node_modules/@adaptivestone/framework/dist/tests/globalSetupVitest',
    ],
    setupFiles: [
      './src/tests/setup.ts', // this is a config files with directory location
      '@adaptivestone/framework/tests/setupVitest.js', // This is the entry point for testing from the  framework
      './src/tests/setupHooks.ts', // This is a local config file (see below)
    ],
  }
```

### Global setup (once per test running)

You are able to provide additional setup and teardown functions to global setup.
Just add your implementation in an additional (or instead of) globalSetup

```js
  test: {
    globalSetup: [
      'node_modules/@adaptivestone/framework/dist/tests/globalSetupVitest',
      './src/tests/globalSetup.ts', // custom file for global setup and teardown
    ],
    // ...
  }
```

### Custom hooks (beforeAll, etc) per test

Testing helpers provide isolation of modules, and we run preparation of the framework and teardown for each test as well.

```js
  test: {
    //...
    setupFiles: [
      './src/tests/setup.ts', // this is a config files with directory location
      '@adaptivestone/framework/tests/setupVitest.js', // This is the entry point for testing from the  framework
      './src/tests/setupHooks.ts', // <-- we are able to provide custom logic there
    ],
  }
```

You can provide any amount of testing configs

Example:

```ts ./src/tests/setupHooks.ts
import { beforeAll, afterAll, beforeEach, afterEach } from "vitest";
import { createDefaultTestUser } from "./testHelpers.ts";

beforeAll(async () => {
  await createDefaultTestUser();
});

afterAll(async () => {
  // do something
});

beforeEach(async () => {
  // do something
});

afterEach(async () => {
  // do something
});
```

### Default User for Testing

You are able to call the creation of a default user. The user is not created by default. You should call it manually.

```js
import {
  defaultUser, // instance of user if default user was created
  defaultAuthToken, // default token for auth if user was created
  createDefaultTestUser, // create default user and populate defaultUser and defaultAuthToken.
} from "@adaptivestone/framework/tests/testHelpers.js";

const { user, token } = await createDefaultTestUser();
// defaultUser - same user
// defaultAuthToken - same token
```

## Using with `node:test`

The framework's test lifecycle is **runner-agnostic**: the setup logic lives in plain async functions (`@adaptivestone/framework/tests/setupFramework.js`) with no vitest dependency, so you can drive it from Node's built-in [`node:test`](https://nodejs.org/api/test.html) runner. `vitest` is an optional peer dependency — node:test users don't need it installed.

Wire it per file (mirrors `setupVitest`):

```ts
import "@adaptivestone/framework/tests/setupNodeTest.js"; // server per file + per-test redis isolation
import { test } from "node:test";
import assert from "node:assert/strict";
import { getTestServerURL } from "@adaptivestone/framework/tests/testHelpers.js";

test("returns 400 on a bad body", async () => {
  const { status } = await fetch(getTestServerURL("/some/endpoint"), { method: "POST" });
  assert.equal(status, 400);
});
```

### The one difference: global Mongo

vitest runs all files in one process and starts the in-memory Mongo once via `globalSetup`. node:test runs **each file in its own process**, so there is no per-file place to start a shared Mongo. Use node:test's [global setup hook](https://nodejs.org/api/test.html#global-setup-and-teardown) (`--test-global-setup`) — the exact analog of vitest's `globalSetup`. Write a tiny entry module:

```ts title="globalSetup.ts"
import {
  startTestMongo,
  stopTestMongo,
} from "@adaptivestone/framework/tests/setupFramework.js";

export async function globalSetup() {
  await startTestMongo(); // sets TEST_MONGO_URI; every test process inherits it
}

export async function globalTeardown() {
  await stopTestMongo();
}
```

Run the whole suite through it (one Mongo, shared across every file):

```bash
node --test --test-global-setup=./globalSetup.ts
```

:::note
`--test-global-setup` is experimental (Stability 1) but ships in every Node ≥ 24 — the framework's own node:test suite uses it. If you'd rather avoid the flag, point `TEST_MONGO_URI` at an external Mongo (a CI service or a local instance) and skip the in-memory server.
:::

The runner-agnostic building blocks (exported from `@adaptivestone/framework/tests/setupFramework.js`):

| Export | Runs | Purpose |
|---|---|---|
| `startTestMongo` / `stopTestMongo` | once per run | in-memory Mongo replica set; sets `TEST_MONGO_URI` |
| `startTestServer` / `stopTestServer` | per file | boot / tear down a server against a fresh DB |
| `setTestRedisNamespace` / `clearTestRedisNamespace` | per test | isolate the cache / rate-limiter keyspace |

`setupVitest` and `setupNodeTest` are thin wrappers that wire these into each runner's hooks.

## Mongo Instance

As the framework is designed to work with MongoDB and provide easy integration with it, it also comes with MongoDB integration in tests.

The integration is done with the help of the [MongoDbMemoryServer](https://github.com/nodkz/mongodb-memory-server) package.

By default, the framework starts the Mongo memory server and stops it afterward. So you can use Mongo during your tests.

### Mongo Tests on ARM64 Machines (Docker)

For ARM64, we have an interesting situation. Mongo Inc. provides binaries for Ubuntu but not for Debian, but official Node images exist for Debian but not for Ubuntu.

To solve this situation, we provide our own Node Docker image based on Ubuntu. You can find it here: [ubuntu-node-docker](https://gitlab.com/adaptivestone/ubuntu-node).

## Running Tests in CI (GitLab)

An important thing about testing is that tests should be executed automatically on every Git commit. That is where CI (Continuous Integration) comes in.

A `.gitlab-ci.yml` sample is below:

```yaml
stages:
  - install
  - checks

install:
  stage: install
  image: registry.gitlab.com/adaptivestone/ubuntu-node:latest
  script:
    - node -v
    - npm install
  artifacts:
    paths:
      - node_modules/
    expire_in: 2 hour

codestyle:
  stage: checks
  image: registry.gitlab.com/adaptivestone/ubuntu-node:latest
  needs:
    - install
  dependencies:
    - install
  allow_failure: true
  script:
    - npm run codestyle

tests:
  stage: checks
  image: registry.gitlab.com/adaptivestone/ubuntu-node:latest
  needs:
    - install
  dependencies:
    - install
  script:
    - npm run test
```

## Running Tests in CI (GitHub)

It is better to look at the [repo](https://github.com/adaptivestone/framework/blob/main/.github/workflows/test.yml).

:::note

The `redis` service below is only needed if your tests exercise the **redis** cache or rate-limiter driver. With the default in-memory cache (and the default `mongo` rate limiter), you can drop the `redis` service and `REDIS_URI` entirely — `clearTestRedisNamespace` is a no-op when no Redis is reachable.

:::

```yml
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: Test

on:
  push:
    branches: ["*"]

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    services:
      redis:
        image: redis:latest
        ports:
          - 6379:6379

    env:
      LOGGER_CONSOLE_LEVEL: "error"
      REDIS_URI: redis://localhost

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: "latest"
          cache: "npm"

      - name: npm clean install
        run: npm ci

      - name: Run Test
        run: npm test

      - name: Upload results to Codecov
        uses: codecov/codecov-action@v5
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
```

## Server instance access

It's possible that in testing you will need to have low-level access to the server itself. We have a helper there too.

```js
import { serverInstance } from "@adaptivestone/framework/tests/testHelpers.js";
```

## Test-only controllers

When testing edge cases — broken handlers, unusual middleware combinations, schemas designed to fail — you don't have to put fixture controllers in your production controllers folder. Register them explicitly via `app.controllerManager.registerController(ControllerClass, prefix?)`.

The framework's `Server.startServer()` accepts a `callbackBefore404` hook that runs after the controller manager initializes the auto-loaded controllers but before the 404 handler is attached, so explicitly registered controllers mount in the right order.

```ts ./src/tests/setupHooks.ts
import { beforeAll } from "vitest";
import { serverInstance } from "@adaptivestone/framework/tests/testHelpers.js";
import BrokenController from "./fixtures/BrokenController.ts";
import FakeAuthMiddleware from "./fixtures/FakeAuthMiddleware.ts";

// If you control startServer yourself, register inside callbackBefore404:
//   await server.startServer(async () => {
//     server.app.controllerManager?.registerController(BrokenController, "broken");
//   });
//
// If you use the framework's standard test setup, register in beforeAll —
// late registration still works for tests because the test's HTTP requests
// only fire AFTER setup completes (so the 404 handler ordering is irrelevant
// in practice; the controller's routes are reachable on Express).
beforeAll(() => {
  serverInstance.app.controllerManager?.registerController(BrokenController, "broken");
});
```

`prefix` is the URL prefix segment — `registerController(BrokenController, "broken")` mounts on `/broken/brokencontroller/*`. Pass `''` (or omit) to mount at `/<classname>/`.

This pattern keeps fixture controllers out of your production controllers folder and gives each test full control over which controllers exist for it.

## HTTP Endpoint Testing

The framework provides a special function `getTestServerURL` to help you construct a full URL for testing.

```js
import { getTestServerURL } from "@adaptivestone/framework/tests/testHelpers.js";
const url = getTestServerURL("/auth");
```

Full example:

```js
import {
  getTestServerURL,
  defaultAuthToken,
} from "@adaptivestone/framework/tests/testHelpers.js";

describe("module", () => {
  describe("function", () => {
    it("test", async () => {
      expect.assertions(1);
      const { status } = await fetch(getTestServerURL("/some/endpoint"), {
        method: "POST",
        headers: {
          "Content-type": "application/json",
          Authorization: defaultAuthToken,
        },
        body: JSON.stringify({
          // request object
          oneData: 1,
          secondDate: 2,
        }),
      }).catch(() => {});
      expect(status).toBe(400);
    });
  });
});
```

## Test Helpers

The framework provides a set of helpers to simplify testing.

```js
import {
  getTestServerURL, // return server URL for testing  getTestServerURL('auth');
  defaultUser, // instance of user if default user was created
  defaultAuthToken, // default token for auth if user was created
  serverInstance, // server instance for low level interaction
  createDefaultTestUser, // create default user and populate defaultUser and defaultAuthToken.
  setDefaultUser, // in case you want to have own user implementation setDefaultUser(yourUser)
  setDefaultAuthToken, // in case you want to have own user implementation setDefaultAuthToken(token)
} from "@adaptivestone/framework/tests/testHelpers.js";
```

## Mock

In most cases, your code depends on external services, but you still need to perform testing. Calling an external service for each test can be expensive and is not necessary. For this problem, Vitest provides mock options. This is when, instead of calling the real SDK of a service, you call a fake function that provides the result without API calls.

[https://vitest.dev/api/vi.html#vi-mock](https://vitest.dev/api/vi.html#vi-mock)

### Mocking a Function

[https://vitest.dev/api/vi.html#mocking-functions-and-objects](https://vitest.dev/api/vi.html#mocking-functions-and-objects)

You can redefine an import for your own import.

```js
vi.doMock("../file.js", () => ({
  fileFunction: async () => ({
    isSuccess: true,
  }),
}));
```

Redefine one method in a file:

```javascript
import S3 from "../S3.js";
vi.spyOn(S3, "validateCreds").mockImplementation(() => true);
```

There are many more mocking options. Please refer to the Vitest documentation for others.

{/*
Manual mocks are defined by writing a module in a **mocks** subdirectory immediately adjacent to the module. For example, to mock a module called user in the models directory, create a file called user.js and put it in the models/**mocks** directory. Note that the **mocks** folder is case-sensitive, so naming the directory **MOCKS** will break on some systems.

:::note
You should call moch load function before performing any operation on it

```js
vi.mock("path");
```

:::

### @google-cloud/translate example (NODE_MODULES)

Assume that we have some translation helper (synthetic example) that just does a translation and registers it in the database to speed it up for the next time.

/src/helpers/translateHelper.js

```js
import { v2 } from "@google-cloud/translate";
const translate = new v2.Translate();

// no error handling because it's an example. You should handle errors in production mode
// no model passing
const translateHelper = async (text, language) => {
  const alreadyTranslatedData = await TranslatedModel.find({ text, language });
  if (alreadyTranslatedData) {
    return alreadyTranslatedData.translatedText;
  }

  const translated = await translate.translate(text, language);
  const data = await TranslatedModel.create({
    text,
    language,
    translatedText: translated[0],
  });
  return translated[0];
};

export default translateHelper;
```

Right now you want to test that the function works correctly. So as @google-cloud/translate.js is a node_modules module, we are creating a file:

```js
/__mocks__/@google-cloud/translate.js
```

The file extends the original Google Translate and overwrites some functions to avoid API calls.

```js /__mocks__/@google-cloud/translate.js
// A manual mock in __mocks__ just exports the fake module — no auto-mock helper needed.
class Translate {
  translate(text, lang) {
    return [`${text}_${lang}`, "this is test"];
  }
}

export default { v2: { Translate } };
```

Now inside your helper test file:

```js
vi.mock('@google-cloud/translate');

import translateHelper from '/src/helpers/translateHelper.js';


describe('mock testing', () => {
  it('should return translated text', async () => {
    expect.assertions(1);
    const translated = await translateHelper("text","fr");
    expect(translated).toBe("text_fr");
  })

  it('should store text in the database', async () => {
    expect.assertions(1);
    const translated = await TranslatedModel.find({text:"text", language:"fr"});
    expect(translated.translatedText).toBe("text_fr");
  })
})


``` 
*/}


---


# Document 14: 10-cli

<!-- Source: 10-cli.md -->

# CLI

The CLI (Command Line Interface) is a part of the framework system that allows you to use the full power of the framework on the command line - manipulate data, import, export, etc.

The framework scans the directory for a list of commands and provides you with the ability to see what commands are available to run.

:::note

CLI commands are part of the [framework inheritance process](03-files-inheritance.md).

:::

## Run Command

You are able to create a group of commands by putting files in a directory. You can see ‘migration’ as an example of grouping commands.

```bash
# On the project level
node src/cli.ts {command}

# OR
npm run cli
```

The command path works the same way as in controllers. Any file will be parsed as a command, and folders will group commands together.
But unlike at the controller level, you are not able to change that behavior.

You are able to create a group of commands by putting files in a directory. You can see ‘migration’ as an example of grouping commands.

```js
commands / migration / create.ts; // migration/create command
commands / migration / migrate.ts; // migration/migrate command
```

## API

You are able to run a command from any place in the framework.

```js
this.app.runCliCommand(commandName: string, args: {}): Promise;
```

Where:
`commandName` - the name of the command that you want to start.
`args` - the arguments that you want to pass to the command.

## Creating Your Own Command

All passed arguments on the command line are parsed with the help of the `parseArgs` module.

```ts
import AbstractCommand from "@adaptivestone/framework/modules/AbstractCommand.js";
import type { CommandArgumentToTypes } from "@adaptivestone/framework/modules/AbstractCommand.js";

class CommandName extends AbstractCommand {
  static get description() {
    return "Some nice description of the command";
  }

  /**
   * You are able to add command arguments for parsing here.
   * https://nodejs.org/api/util.html#utilparseargsconfig
   */
  static get commandArguments() {
    return {
      id: {
        type: "string",
        description: "User ID to find the user",
      },
      email: {
        type: "string",
        description: "User email to find/create the user",
      },
      password: {
        type: "string",
        description: "New password for the user",
      },
      roles: {
        type: "string",
        required: true, // make sure that command will ask this from user
        description:
          "User roles as a comma-separated string (--roles=user,admin,someOtherRoles)",
      },
      update: {
        type: "boolean",
        default: false,
        description: "Update the user if they exist",
      },
    } as const; // <- this is important for type generation
  }

  static isShouldInitModels = true; // Default value. Can be omitted.

  /**
   * If true, then this command will get model paths with inheritance.
   */
  static isShouldGetModelPaths = true;

  /**
   * Return the name of the connection that you want to use.
   */
  static getMongoConnectionName(commandName, args) {
    return `CLI: ${commandName} ${JSON.stringify(args)}`;
  }

  async run() {
    // CommandArgumentToTypes will generate types from  commandArguments description
    const { id, email, password, roles, update } = this
      .args as CommandArgumentToTypes<typeof CommandName.commandArguments>;

    return new Promise((resolve, reject) => {});
  }
}

export default CommandName;
```

:::note Model commands wait for the database connection

`static isShouldInitModels = true` (the default) makes the framework load and initialize your models before the command runs **and wait for the MongoDB connection to be ready** — so your command's first query never races a not-yet-established connection (the cause of intermittent "buffering timed out" errors). You don't need to add your own connection-readiness check.

Set it to `false` for commands that don't touch the database (e.g. code generation, crypto helpers) so they start instantly without connecting. `static isShouldGetModelPaths` loads model _paths_ only (for type/path resolution) — it does not initialize models or open a connection.

:::

:::tip

For boolean types, we also support negative values (without a prefix).

```js
      update: {
        type: "boolean",
      },
```
```bash
node src/cli.ts ourCommand --update
```
Update Argument will by true

```bash
node src/cli.ts ourCommand --no-update
```
update argumant will be false 

:::

## Framework Commands

The framework comes with a few built-in commands.

### Migration

Migration commands allow you to migrate some data to another, or fill the database with some data.

The key point here is that a migration is executed only once per file.

:::tip

You can use migrations for different cases, not only to modify data in the database.

:::

#### Creating a Migration

The migration command comes with a template to generate a migration.

```js
node src/cli.ts migration/create --name={someName}
```

After creating the migration, please edit it and implement any logic that you want here.

#### Applying a Migration

```js
node src/cli.ts migration/migrate
```

A migration is executed in the order it was created. It is executed only once per life.

### DropIndex

Sometimes it is very useful to drop indexes on already created models. For example, you created a unique index for non-required fields and missed that `null` values also should be unique per collection.

The framework will take care of creating indexes based on your model on the next start, so you can drop an index and be sure that it will be recreated after.

#### Run dropindex

```js
node src/cli.ts dropindex --model={modelName}
```

### SyncIndexes

Synchronizes the indexes defined in the models with the real ones indexed in the database. The command will remove all indexes from the database that do not exist in the model OR have different parameters. Then it will create new indexes.

:::warning

This can be a dangerous command in case you have some unique index features.

:::

#### Run SyncIndexes

```js
node src/cli.ts SyncIndexes
```

### CreateUser

The `createuser` command creates a new user.

#### Run CreateUser

```js
node src/cli.ts createuser --email=somemail@gmail.com --password=somePassword --roles=user,admin,someOtherRoles
```

Only email and password are required.

You are able to update a user as well. You need to specify the email or user ID to find the user and the '--update' flag to allow user updates.

### Generate Random Bytes

In some cases, you need a random byte string. This command helps you to generate a random byte string.

#### Run Generate Random Bytes

```js
node src/cli.ts generateRandomBytes
```

or

```js
npm run cli generateRandomBytes
```

### Generate TypeScript Types

Generates two kinds of TS source from the framework's introspection:

1. **`genTypes.d.ts`** at the project root — augments `IApp` so `getConfig('foo')` and `getModel('Bar')` are typed.
2. **`<File>.routes.gen.ts`** next to every controller — typed `<MethodName>Request` aliases for handler signatures (per-route schema output, middleware-provided `appInfo` fields, etc.). The middleware chain comes from `RouteRegistry.flatten()` — same matcher the runtime uses, so types match runtime behavior. See [Routes → Typed handler signatures (codegen)](06-Controllers/02-routes.md#typed-handler-signatures-codegen) for usage.

#### Run Generate TypeScript Types

```bash
node src/cli.ts generatetypes
# or
npm run cli generatetypes
```

#### Recommended setup

Add to `package.json`:

```json
"gen": "node src/cli.ts generatetypes",
"check:types": "npm run gen && tsc --noEmit"
```

Add to `.gitignore`:

```
genTypes.d.ts
**/*.routes.gen.ts
```

Gen files regenerate on every type-check — no postinstall hook needed.

#### When to run codegen

Codegen only affects **types**, never runtime — so you run it whenever something that changes a handler's or `IApp`'s type shape changes, then type-check. In practice you wire it into `check:types` and forget it's there. The decision matrix:

| You changed… | Run `gen`? | Why |
|---|---|---|
| A route's `request:` / `query:` schema | **Yes** | `req.appInfo.request` / `.query` types change |
| A route path (added `:param` / `{*splat}`, renamed) | **Yes** | `req.params` and the `<Method>Request` alias change |
| A controller's `static get middleware()` chain | **Yes** | which `provides` fields land on `req.appInfo` changes |
| A middleware's `static get provides()` | **Yes** | downstream `req.appInfo` types change |
| Added / renamed a model or config | **Yes** | `getModel('X')` / `getConfig('Y')` typings change |
| Only handler body logic (no signature/schema change) | No | the generated types are unchanged |
| Prose, comments, formatting | No | nothing type-bearing changed |

When in doubt, just run it — it's idempotent and cheap. The standard wiring (`"check:types": "npm run gen && tsc --noEmit"`) regenerates before every type-check, so you never run it by hand in CI. See [Routes → Typed handler signatures](06-Controllers/02-routes.md#typed-handler-signatures-codegen).

### Generate OpenAPI

Generates an OpenAPI 3.1 document from your controllers (paths, parameters, request bodies, security, tags) — derived from the route definitions you already write. Opens no database/network connection and binds no port, so it's safe in CI.

```bash
node src/cli.ts openapi                       # print to stdout
node src/cli.ts openapi --output openapi.json # write to a file
```

See the [OpenAPI chapter](17-openapi.md) for what's documented, how schemas are introspected, and how middlewares contribute security schemes.

### List Routes

Prints your project's route tree — every mounted method, full path, path/splat parameters, and the middleware chain that runs for each route — by walking the same route registry the server logs at boot. Like `openapi`, it opens no database/network connection and binds no port.

```bash
node src/cli.ts routes
```

Example output:

```text
Registered routes:
/
├── GET     /
└── auth  (mw: GetUserByToken, RateLimiter)
    ├── login
    │   └── POST    /auth/login  [request]
    └── verify
        └── POST    /auth/verify  [request]

3 route(s) across 5 node(s) in the tree.
```

Reading the annotations:

- **`(mw: A, B)`** — middleware newly attached at this node. Inherited middleware from parent nodes is shown as `pmw:` so each route line is self-contained about what runs before it.
- **`[request]` / `[query]`** — this route **validates** a request body / query string. They are presence flags, not the schema itself: a route either declares a validator or it doesn't. To see the actual field shapes, run `openapi` — it resolves each schema to JSON Schema (where the validator supports introspection). Imperative validators such as `defineSchema` have no introspectable shape, so they appear here as `[request]` and degrade to a placeholder in the OpenAPI document.

Useful for answering "what's actually mounted in my app?" without starting the server. For the full request/response contract, use [`openapi`](#generate-openapi).


---


# Document 15: 11-cache

<!-- Source: 11-cache.md -->

# Cache

The cache subsystem is designed to store some values for quick access and a unified interface. It is useful when you have some values grabbed from an external API or some stuff that requires a lot of calculation and does not change from time to time.

Caches have an expiration time, and the developer should not worry about checking it. If a value has expired or does not exist, a callback will be executed, and its return value will be used as the value to store in the cache.

:::note

The cache subsystem handles all values and takes care of serialization/deserialization.

:::

## API

The API is simple:

```ts
  async getSetValue(
    key: string,
    onNotFound: () => Promise<any>,
    storeTime: number, // in seconds
  ): Promise<any>;
```

By default, the store time is 5 minutes. A store time of `0` means **"don't cache"** — the callback runs on every call and nothing is written.

Example:

```javascript
const cacheTime = 60 * 5; // seconds
const someValueFromCache = await this.app.cache.getSetValue(
  "someKey",
  async () => {
    const someValue = await someLongAsyncOperation();
    return someValue;
  },
  cacheTime // in seconds
);
```

:::note

You can request the same value multiple times, and only one callback will be executed. All other calls will be resolved as a Promise (the same promise).

```js
const promiseArr = [];
for (let n = 0; n < 100; n++) {
  promiseArr.push(
    this.app.cache.getSetValue(
      "someKey",
      async () => {
        // Will be called once! Other calls will find that "someKey" is already processing and return the same Promise.
        const someValue = await someLongAsyncOperation();
        return someValue;
      },
      3600
    )
  );
}
```

Please note that it works that way **per process**, as checking promises happens at the process level and is not synchronized via a master process.

:::

## Drivers

The cache is built on a small `CacheDriver` interface (`get` / `set` / `del`), with two first-party drivers:

- **`memory`** — **the default.** A per-process `Map` with per-key TTL. Needs no external service, so a plain install works out of the box and never loads `@redis/client`. Because it is per-process, each clustered worker has its own cache — fine for development and single-node deployments.
- **`redis`** — a shared cache backed by Redis. Use this for **multi-node deployments** (or anywhere multiple processes must see the same cached values). It lazy-loads `@redis/client`, which is an **optional peer dependency** — install it yourself (`npm i @redis/client`) when you select this driver.

The orchestration around the driver — namespacing, single-flight request dedup, serialization, and fail-soft degradation (a cache outage degrades to running your callback, it never fails the request) — is identical across drivers.

## Configuration

The driver is selected in `config/cache.ts`:

```ts title="config/cache.ts"
export default {
  // 'memory' (default) or 'redis'. Overridable via the CACHE_DRIVER env var.
  driver: (process.env.CACHE_DRIVER || "memory") as "memory" | "redis",
};
```

Two related settings live in `config/redis.ts` rather than here, because they are **shared with the rate limiter**:

- **`namespace`** — a key prefix applied to **every** cache (and rate-limiter) key, regardless of driver. Despite living in `redis.ts`, it is _not_ redis-specific: the in-memory driver prefixes its keys with it too. Think of it as a keyspace/tenant label (e.g. per environment). Keeping it in one place means the cache and rate limiter never drift apart, and the test helper `setTestRedisNamespace` can isolate both with a single switch.
- **`url`** — the Redis connection string, used only by the `redis` backends (this cache and the rate limiter's redis driver share one client). Irrelevant when both run on non-redis drivers.

:::tip Custom driver

You can supply your own backend by setting `driver` to an object implementing `CacheDriver` (`get`, `set`, `del`) instead of a string.

:::


---


# Document 16: 12-email

<!-- Source: 12-email.md -->

# Sending Emails

:::warning

As of framework version 5.0, we have moved the email module to a separate package: [@adaptivestone/framework-module-email](https://www.npmjs.com/package/@adaptivestone/framework-module-email).

:::

The email subsystem is based on [Nodemailer](https://github.com/nodemailer/nodemailer). In addition, we are using [Juice](https://www.npmjs.com/package/juice) to inline CSS and [html-to-text](https://www.npmjs.com/package/html-to-text) to generate text from the HTML of files.

:::note

Sadly, email clients are outdated and do not support a lot of web features. Some clients do not even support “style” tags. That is why all styles should be inlined.

:::

## Installation

```bash
npm i @adaptivestone/framework-module-email
```

## Templates

A template is a folder of files; each file's extension selects the engine that renders it. For each email you provide an HTML version, a subject, and (optionally) a text version, as separate files inside the template directory.

If the text version of the email is not provided, it will be generated from the HTML version by removing all HTML tags with the help of the [html-to-text](https://www.npmjs.com/package/html-to-text) package.

The template directory is located at `src/services/messaging/email/templates/{templateName}` in your project. You can change it in the config.

By default the module ships only plain-text engines — `html`, `text` and `css` (files are read as-is):

```js
html.html; // HTML markup of the email
subject.text; // Subject to generate
text.text; // Text version of the email (optional)
style.css; // Styles to inline inside the HTML
```

To use a real template language such as Pug, register its engine first (see [Template engines](#template-engines)); then your files can be `html.pug`, `subject.pug`, and so on.

## Template engines

:::warning Breaking change in v2

Before v2, Pug was bundled and `.pug` templates worked out of the box. As of **v2 the module ships no template-engine dependency** — only the plain-text `html`, `text` and `css` engines. To keep using `.pug` (or any other language) you must install that engine and register it yourself.

:::

Register an engine by mapping a file extension to a render function. The function receives the absolute path to the template file and the render data, and returns the rendered string (sync or async):

```js
import pug from "pug";
import ejs from "ejs";
import Mailer from "@adaptivestone/framework-module-email";

// Pug — was bundled by default before v2; now opt-in
Mailer.registerTemplateEngine("pug", (fullPath, data) =>
  pug.compileFile(fullPath)(data),
);

// any engine works the same way
Mailer.registerTemplateEngine("ejs", (fullPath, data) =>
  ejs.renderFile(fullPath, data),
);
```

### Where to register

Engines live in a **single process-wide registry** shared by every `Mailer` instance, so register them **once at process startup, before any email is sent** — not per request and not per `Mailer` instance.

The natural place is the worker bootstrap (`src/server.ts`), the file each worker process runs. Register before `startServer()`:

```js
// src/server.ts
import Server from "@adaptivestone/framework/server.js";
import Mailer from "@adaptivestone/framework-module-email";
import pug from "pug";
import folderConfig from "./folderConfig.ts";

Mailer.registerTemplateEngine("pug", (fullPath, data) =>
  pug.compileFile(fullPath)(data),
);

const server = new Server(folderConfig);
await server.startServer();
```

:::note

The registry is per **process**. If your app uses the cluster manager (`src/index.ts` forking workers), register in `src/server.ts` (which every worker runs), not in the master `src/index.ts` (which never sends mail).

:::

### Registering more than once

`registerTemplateEngine` can be called as many times as you like:

- **Different extensions accumulate** — call it once per engine you want (`pug`, `ejs`, `mjml`, …).
- **The same extension overrides** — the last registration for a given extension wins, so you can replace a built-in or re-register safely. There is no error on re-registration.
- Extensions are normalized, so `"pug"`, `".pug"` and `".PUG"` all target the same engine.

### Helpers

- `Mailer.registerTemplateEngine(extension, engine)` — register/override an engine for a file extension (leading dot optional, case-insensitive).
- `Mailer.unregisterTemplateEngine(extension)` — remove an engine; returns `true` if one was removed.
- `Mailer.hasTemplateEngine(extension)` — check whether an engine is registered.

### Inline Images

By default, the framework email module does not inline images and keeps the links as they are.
But if you want to inline some images, you can use the "data-inline" attribute in the "img" tag.

```html
<img src="/cats.jpg" data-inline />
```

The image path is relative to your project's "src/services/messaging/email/resources" folder.

:::note
The best practice is to put your images on a CDN.
:::

### Template Variables

Each template has these variables:

- `locale` - the current locale of the request.
- `t` - the translate function from i18n. Can be a dummy function if i18n is not provided.
- `globalVariablesToTemplates` - from the config.
- User-provided variables (see the API section).

## API

### Init Mailer

```js
import Mailer from "@adaptivestone/framework-module-email";

const mail = new Mailer(
  this.app,
  "recovery", // template name
  {
    // variables for the template. These are user-provided variables. They will be merged with the default variables.
    oneTemplateVariable: "1",
    anotherTemplateVariable: "2",
  },
  req.appInfo.i18n
);
```

Inside the template, `oneTemplateVariable` and `anotherTemplateVariable` will be available as top-level variables.

```pug
p #{oneTemplateVariable} #{anotherTemplateVariable}
```

### Send Email

```js
const result = await mail.send(
  "some@email.com", // To
  "optional@from.com", // OPTIONAL. From email. If not provided, it will be grabbed from the config.
  {} // OPTIONAL. Any additional options for Nodemailer: https://nodemailer.com/message/
);
```

### Send Raw

For advanced usage (your own templates, mail headers, attachments), another low-level method exists.

```js
import Mailer from "@adaptivestone/framework-module-email";

const result = await Mailer.sendRaw(
  this.app, // framework app
  "to@email.com", // To
  "email subject", // topic
  "<html><body><h1>Email html body</h1></body></html>", // HTML body of the email
  "Email text body", // OPTIONAL. If not provided, it will be generated from the HTML string.
  "from@email.com", // OPTIONAL. From email. If not provided, it will be grabbed from the config.
  {} // OPTIONAL. Any additional options for Nodemailer: https://nodemailer.com/message/
);
```

### Render Template

In some cases, you may want to render templates to a string for future usage. For example, to send an email via Gmail OAuth2 authorization on behalf of a user.

```js
const { subject, text, inlinedHTML, htmlRaw } = await mail.renderTemplate();
```

## Configuration

Please look at the ‘config/mail.ts’ file for all configuration options.

### Environment Variables

Here are the most important environment variables:

```js
EMAIL_HOST; // smtp.mailtrap.io by default
EMAIL_PORT; // 2525 by default
EMAIL_USER;
EMAIL_PASSWORD;
EMAIL_TRANSPORT; // smtp by default
```


---


# Document 17: 12-resize

<!-- Source: 12-resize.md -->

# Image Resizing

Lazy image resizing for the framework, shipped as a separate package: [@adaptivestone/framework-module-resize](https://www.npmjs.com/package/@adaptivestone/framework-module-resize). Upload only the **original**; generate resized variants on demand with [`sharp`](https://sharp.pixelplumbing.com).

The **read path** decides — per requested size + format + filters — whether a preview is ready or missing. Ready ones return immediately; missing ones are **enqueued** and generated by a separate **worker**. Everything the module touches (queue transport, storage, media store, lock provider) is a **swappable driver** wired in one constructor literal.

## Installation

```bash
npm i @adaptivestone/framework-module-resize
```

Requires Node `>=24` and the framework/mongoose peers (mandatory). The AWS SDKs are **optional peers** — install only the driver you use. Each is resolved **only** when you import its driver subpath, so the main entry never loads the AWS SDKs, and a missing peer fails loudly at your own import line at bootstrap, not at first I/O.

| You use… | Also install |
|---|---|
| **S3 storage** (`/storage/s3.js`) | `@aws-sdk/client-s3` `@aws-sdk/s3-request-presigner` |
| **SQS transport** (`/transports/sqs.js`) | `@aws-sdk/client-sqs` `sqs-consumer` |
| Mongo transport / framework media store / framework locks | nothing |

### Scaffold the integration files

The framework discovers models and commands by scanning your `src/` folder, so a few thin files must live in your app. Generate them once:

```bash
npx @adaptivestone/framework-module-resize resize-scaffold
```

It emits (into `process.cwd()`, or `--out <dir>`), **never overwriting** without `--force`:

| File | What it is |
|---|---|
| `src/resizer.ts` | the construction site — `new Resizer({ … })` |
| `src/models/ResizeTask.ts` | thin `class ResizeTask extends ResizeTaskModel {}` shim (Mongo transport) |
| `src/commands/ResizeWorker.ts` | one-line re-export of the module's worker command |
| `src/config/resize.ts` | editable config that spreads the module defaults |

The shims are **not vendored copies** — schema and behavior stay in the npm package (auto-updates, no drift). Other flags: `--check` (CI drift check), `--eject` (full editable model), `--eager` (eager-mode hosts), `--force`, `--out <dir>`.

## How it works

```
upload ─▶ store the ORIGINAL only (no previews baked at upload)
  read ─▶ resolve({ media, sizes }) ─┬─ ready?   → return the URL now
                                     └─ missing? → enqueue + return a placeholder/original
worker ─▶ download original → beforeSteps → per-variant resize + variantSteps + encode → upload
       ─▶ append preview to the media doc
  next read ─▶ ready
```

Generated **previews** live as metadata on the host's media document (`previews[]`) — the source of truth for what is ready. `resolve()` returns a **decision** (`ready[]` + `missing[]`); missing variants are enqueued while the read returns without ever blocking on `sharp`. This keeps `sharp` + storage I/O off your HTTP create/update handlers.

## Quick start (lazy mode, Mongo + S3)

**1. Wire the Resizer** in the scaffolded `src/resizer.ts`. All drivers are injected in one visible literal and fixed at construction — one Resizer per process (a second `new Resizer()` throws).

```ts
// src/resizer.ts — imported by src/server.ts so it runs in EVERY process (API + worker)
import { Resizer } from '@adaptivestone/framework-module-resize';
import { MongoTransport } from '@adaptivestone/framework-module-resize/transports/mongo.js';
import { S3Storage } from '@adaptivestone/framework-module-resize/storage/s3.js'; // optional AWS peers resolved only here

export const resizer = new Resizer({
  transport: new MongoTransport(),           // or new SqsTransport({ queueUrl, region }); omit for eager-only
  storage: new S3Storage({                   // REQUIRED — shipped driver or any custom ResizeStorage
    bucketPublic: 'my-cdn',
    bucketPrivate: 'my-originals',
    publicUrl: 'https://cdn.example.com',
  }),
  // mediaStore / lockProvider omitted → framework-backed defaults
  pipelines: {
    default: {},
    listing: { beforeSteps: [blurPlates] },  // async detector, applied once to the source
  },
  hooks: {
    resolveSizes:     (sizes, ctx) => ctx.entity === 'event' ? [...sizes, { fit: true }] : sizes,
    formatPublicUrls: (decision, ctx) => toHostDto(decision, ctx),   // your response shape + placeholders
  },
});
```

**2. Import it once from `src/server.ts`** so it runs in both the API and worker processes:

```ts
import './resizer.ts';
```

**3. Set your media model name** in `src/config/resize.ts` — the one required field:

```ts
import defaultResizeConfig from '@adaptivestone/framework-module-resize/config/resize.js';

export default {
  ...defaultResizeConfig,
  mediaModelName: 'File', // your host media model, e.g. 'File' or 'Media'
};
```

Your media model must carry `original` (incl. `width`/`height`) and `previews[]` (incl. `filters`/`fit`). That schema is host-owned; to avoid drift the module exports an **opt-in** `as const` fragment you can spread in:

```ts
import { resizeMediaSchemaFragment } from '@adaptivestone/framework-module-resize';
class File extends BaseModel {
  static get modelSchema() { return { ...existingFields, ...resizeMediaSchemaFragment } as const; }
}
```

**4. Run the worker** as a separate process (gated by `worker.enabled`):

```bash
npm run cli ResizeWorker
```

**5. Read** from your DTO builders. No `app` argument — the module reads the ambient app instance. `resolve` returns both the raw `decision` and the `output` of your `formatPublicUrls` hook:

```ts
import { resizer } from '../resizer.ts';   // or: getResizer()

const { output } = await resizer.resolve({
  media: fileDoc,
  pipeline: 'listing',
  sizes: [
    { width: 1760, height: 990 },
    { width: 620 },
    { fit: true },
    { width: 300, height: 300, filters: { blur: 40 } },
  ],
  ctx: { entity: 'event', isOwner },
});
return output; // your own shape, produced by formatPublicUrls
```

## Modes: lazy vs pre-warm vs eager

All three modes drive the **same resize core** and write the same `previews[]` shape, so you can switch later with no data migration, or mix them.

| | **Lazy** (default) | **Pre-warm** | **Eager** |
|---|---|---|---|
| Generate | on first read; `resolve()` enqueues missing | at upload; `prewarm()` enqueues the catalog | inline at upload via `resizer.generate(...)` |
| Needs | transport + `ResizeWorker` + `ResizeTask` + locks | same as lazy | storage + media model only — **no** queue/worker |
| Best for | high volume, fast uploads, large catalogs | fast uploads **and** a warm cache by first read | low/bursty volume, small fully-used catalogs |

:::tip Default to lazy

It keeps uploads fast and only does work that's actually needed. **Choose eager** when your app is low-volume, your size catalog is small and fully used, and you'd rather every image be ready the instant an upload finishes. The stored shape is identical, so you can switch later or mix the three.

:::

**Pre-warm** keeps the lazy wiring (transport + worker) but pushes the catalog into the queue at upload, so previews are usually ready by the first read. It never blocks and never throws:

```ts
// upload handler, after the media doc is created:
await resizer.prewarm({ media: fileDoc, sizes: getListingSizes(), pipeline: 'listing' });
// → { enqueued } = how many variants were handed to the queue
```

**Eager** constructs the Resizer **without** a `transport` and generates synchronously from the upload handler (`ctx` reaches pipeline steps here, unlike the queued worker):

```ts
const { previews } = await resizer.generate({
  media: fileDoc,
  sizes: getEventMediaSizes(),   // your catalog
  pipeline: 'listing',
  // persist: true (default) → $push previews + backfill dims; false → returns them for you to store
});
```

## Drivers & seams

Four seams, each a single active strategy fixed at construction. Two ship drivers; two default to framework-backed drivers when omitted, so a standard host wires only `transport` + `storage`. Every driver lives behind its own package subpath (the core entry never loads driver deps).

| Seam | Option | Shipped | Subpath import |
|---|---|---|---|
| Queue transport | `transport?` | `MongoTransport`, `SqsTransport` | `…/transports/mongo.js`, `…/transports/sqs.js` |
| Storage | `storage` **(required)** | `S3Storage` | `…/storage/s3.js` |
| Media store | `mediaStore?` | `FrameworkMediaStore` (default) | `…/mediaStore/framework.js` |
| Lock provider | `lockProvider?` | `FrameworkLockProvider` (default) | `…/locks/framework.js` |

Reach the process-wide instance anywhere via `getResizer()` (throws a clear error if none was constructed).

**Custom driver = implement the interface.** Any seam takes a plain object (or class) that satisfies its contract — no `app` parameter; it closes over its own client:

```ts
new Resizer({ /* … */, storage: {
  download: (ref) => s3.getObject(ref.bucket!, ref.key),
  upload: async ({ key, body, contentType, visibility }) => {
    const bucket = visibility === 'public' ? 'my-cdn' : 'my-originals';
    await s3.putObject(bucket, key, body, contentType);
    return { bucket, key };               // ← persisted onto the preview/original
  },
  publicUrl: (ref) => `https://cdn.example.com/${ref.key}`,   // pure; no I/O
  signedUrl: (ref, ttl) => s3.getSignedUrl(ref.bucket!, ref.key, ttl),
}});
```

Contract types (`QueueTransport`, `ResizeStorage`, `MediaStore`, `LockProvider`, …) are exported from the main entry. The shipped `S3Storage` / `SqsTransport` options are listed in the [README](https://github.com/adaptivestone/framework-module-resize#drivers--seams).

## Pipelines & hooks

**Pipelines** are named per-media-type pixel work, selected per read call by name. The worker runs in a separate process, so the task carries only the pipeline **name** — the worker resolves the functions from its own registry.

```ts
pipelines: {
  photo: {
    beforeSteps:  [detectAndBlurPlates, detectAndBlurFaces],  // run ONCE on the source, before any resize
    variantSteps: [(img, { variant }) => variant.filters?.blur ? img.blur(Number(variant.filters.blur)) : img],
  },
  avatar: {},                                                 // no special processing
}
// later / from another module: getResizer().registerPipeline('premium', { … })  (last-wins per name)
```

- **`beforeSteps`** — ordered, awaited, once per task on the source buffer. The home for detection metadata and pixel redaction (plate/face blur) that must apply to every variant. A throwing step fails the task.
- **`variantSteps`** — ordered per-variant chain, after resize, before encode. The home for keyed `filters` and anything sized relative to the output.

:::warning Watermark in variantSteps

Put a watermark in `variantSteps`, **not** `beforeSteps`. Baked onto the original once, a watermark scales down with each variant and becomes unreadable on small sizes.

:::

:::note ctx does NOT cross the queue

In the lazy worker `ctx === {}` — the task carries only `{ mediaId, pipeline, previews }`. Durable per-media data a step needs must be read from the loaded `media` doc. The full caller `ctx` reaches steps **only** in eager mode (`generate`, same process).

:::

**Hooks** are the cross-cutting seams. Taps run in registration order, awaited sequentially, and are error-isolated (a throwing tap is logged, never breaks the read/worker flow).

| Hook | Kind | Runs where |
|---|---|---|
| `resolveSizes` | waterfall | read path (real `ctx`) |
| `beforeEnqueue` | waterfall | read path (real `ctx`) |
| `formatPublicUrls` | waterfall | read path (real `ctx`) |
| `onPreviewGenerated` | observer | worker (`ctx === {}`) |
| `afterTaskComplete` | observer | worker (`ctx === {}`) |
| `onTaskFailed` | observer | per failed attempt (will retry) |
| `onTaskDeadLettered` | observer | task exhausted `maxAttempts` (host can alert/page) |

Register at construction (`hooks:`) or later via `getResizer().hook(name, fn)`. Taps are **typed** (`HookSignatures`): each name infers its exact signature, so a wrong argument or return shape is a compile error instead of a silent `any`. In every observer the `task` argument is the transport-agnostic `LeasedTask` (`{ taskId, mediaId, pipeline, previews }`) on **both** transports — never a raw driver document — so a host tap is portable. Every observer is **also** mirrored on the framework event bus as `resize:<name>` (fire-and-forget) for ecosystem subscribers.

## Sizes & identity

A size becomes a canonical **size key** via `getSizeKey`, and the full lookup/lock **identity** is `sizeKey:format:filterSig`. Filters are part of identity (empty → `none`), so a blurred variant is a distinct object.

| Size input | Size key | Meaning |
|---|---|---|
| `{ width: 300, height: 300 }` | `300x300` | cropped (cover) |
| `{ width: 620 }` | `620w` | width-only (banner/strip) |
| `{ height: 400 }` | `400h` | height-only |
| `{ fit: true }` | `fit` | uncropped ("contain"), bounded by `config.maxSize` |
| `{ width: 300, height: 300, filters: { blur: 40 } }` | `300x300` + `blur:40` in identity | keyed alternate rendering |

The **host owns the size catalogs** per entity, injected via `resolveSizes` + per-call `sizes`.

:::warning Security: the catalog is an allowlist

Never pass raw client-supplied dimensions into `sizes` — resolve them against a fixed per-entity catalog first, or you invite arbitrary-resize resource abuse. The module owns the identity key; the host owns which sizes are permitted.

:::

## Configuration

`src/config/resize.ts` (scaffolded, editable) spreads the module defaults and is deep-merged over them by `getResizeConfig()` — override any knob at any depth. **Arrays REPLACE**; nested objects merge field-by-field. The most-touched knobs:

| Key | Default | Notes |
|---|---|---|
| `mediaModelName` | — (**required**) | your host media model name (`'File'`/`'Media'`) |
| `formats` | `['jpeg','webp','avif']` | generated formats |
| `maxSize` | `{ width: 2000, height: 1200 }` | the `fit` cap |
| `encode.quality` | `{ jpeg: 80, webp: 82, avif: 64 }` | per-format — never reuse one int across codecs |
| `worker.enabled` | `false` | gate the worker process (env-driven in host) |
| `queue.maxAttempts` | `5` | delivery count before dead-letter (like SQS `maxReceiveCount`) |
| `queue.taskTimeoutMs` | `600000` | `handleTask` is raced against this; on timeout the task is failed and the slot freed (Mongo transport) |

Storage buckets/URLs and the SQS queue URL are **not** config — they are driver options passed to `new S3Storage({...})` / `new SqsTransport({...})`. See the [full config reference](https://github.com/adaptivestone/framework-module-resize#config-reference) for every knob (encode, limits, queue lease/backoff, worker concurrency).

## Operations

**`ResizeTask` lifecycle** (Mongo transport): `pending → processing → completed | dead`. Retries are capped at `queue.maxAttempts`, then the task is **dead-lettered** (`status:'dead'`) — the lease never reclaims a task past the cap, so no crash-loop runs forever. (SQS uses its native DLQ instead.)

**Dead-letter replay** is a host op — reset the row:

```ts
ResizeTask.updateOne({ _id }, { $set: { status: 'pending', attempts: 0, leaseExpiresAt: null } });
```

**Delivery is at-least-once** (both transports); the worker is **idempotent** — re-running a task for an already-generated identity skips via the existing-preview check, never duplicates.

:::warning SVG sanitization is host-owned

**SVG originals are pass-through** — when `original.contentType === 'image/svg+xml'` the read path serves the original at every requested size/format and never resizes or enqueues. Sanitize SVGs at upload, before storing.

:::

## Host responsibilities

The module owns the resize core; the host owns everything domain-specific:

- The public **response DTO shape** (via `formatPublicUrls`).
- **Which domain models** attach media and the **size catalogs** per entity (via `resolveSizes` + per-call `sizes` — treat catalogs as allowlists).
- **Data migration** from any legacy preview schema.
- **Domain image analysis** — NSFW/object detection, plate/face blur, watermark, masking (inject via pipeline `beforeSteps`/`variantSteps`).
- **Permissions** — who may delete/replace media; the host may opt a read into a signed-original URL via `ctx`.
- **SVG sanitization** and **deleting media / storage cleanup** (the module appends previews but never deletes them).

For the exhaustive tables (every driver option, config knob, and hook signature) see the [README](https://github.com/adaptivestone/framework-module-resize#readme).


---


# Document 18: 13-deploy

<!-- Source: 13-deploy.md -->

# Deploy

There are multiple ways to deploy your app.

## On a Server

This is the simplest way. There are multiple options here, but we are recommending a PM2 + Nginx setup.

### PM2 Setup

You need to install PM2 and make it autoload.

```bash
npm install pm2@latest -g
pm2 startup
```

Then load your code to the server and go to the server directory.

```bash
NODE_ENV=production pm2 start --name YOUR_APP_NAME src/index.ts
pm2 save
```

That is all from the Node.js side. The app is already there and listening on localhost:3300.

### Nginx Setup

```
server {

	root /var/www/YOUR_FOLDER/src/public;

	server_name YOUR_SERVER_NAME;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ @backend;
	}

	location @backend {
		proxy_pass http://localhost:3300;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection 'upgrade';
		proxy_set_header Host $host;
		proxy_set_header  X-Forwarded-For $remote_addr;
		proxy_cache_bypass $http_upgrade;
	}

}
```

## Docker

You can create a Docker image with all your data and run it on any server, including Kubernetes.

The Dockerfile can look like this:

```dockerfile
FROM node:latest
RUN mkdir -p /opt/app && chown -R node:node /opt/app
WORKDIR /opt/app
COPY --chown=node:node package.json package-lock.json ./
USER node
RUN npm ci
COPY --chown=node:node src/ ./src/
EXPOSE 3300
CMD [ "node", "src/index.ts"]
```

To build it, use:

```bash
docker build --platform linux/amd64 -t YOUR_REPO_NAME:TAG .
```

You can also use `buildx`:

```bash
docker buildx build --platform linux/amd64,linux/arm64 -t YOUR_REPO_NAME:TAG . --push
```

Then you can run it:

```bash
docker run -it -p 3300:3300 YOUR_REPO_NAME:TAG
```


---


# Document 19: 14-helpers

<!-- Source: 14-helpers.md -->

# Helpers

The framework provides some helpers to make your code easier to work with.

## App instance

You can access the app instance from anywhere.

```ts
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
```

The app instance is the core of the framework, allowing you to retrieve models, configurations, and more.

```ts
const Model = appInstance.getModel("ModelName");
const s3config = appInstance.getConfig("s3");
// etc
```

### `getAppInstance()` (recommended for early code)

The `Server` constructor is what sets the `appInstance` singleton. Code that reads the raw `appInstance` binding **before** the `Server` is constructed — module scope, lazy imports, external modules — sees `undefined` and fails later with an opaque `TypeError: cannot read properties of undefined` that gives no hint at the cause.

Prefer the `getAppInstance()` getter in that code: it returns the same singleton, or throws a guided error when nothing is set yet.

```ts
import { getAppInstance } from "@adaptivestone/framework/helpers/appInstance.js";

const app = getAppInstance();
const Model = app.getModel("ModelName");
```

If the singleton is not set, the getter throws:

```text
App instance is not initialized yet — construct the Server first (its constructor sets the singleton). In tests, use setAppInstance() to inject one and resetAppInstance() to clear it.
```

:::note Test hooks

The same module exports `setAppInstance(app)` and `resetAppInstance()` for tests that need to run app-instance-dependent code without a full `Server`. `setAppInstance` throws if an instance is already set (only one `Server` per process is supported), and `resetAppInstance()` clears it. Prefer per-file isolation — as the shipped vitest setup does — and reach for `resetAppInstance()` only when a runner can't isolate per file (it does **not** clean up mongoose-registered models, redis client state, or env vars).

:::

## Redis connection

A simplified way to connect to Redis. This helper loads the configuration and adds shutdown hooks.

:::note

`@redis/client` is an **optional peer dependency**. Install it (`npm i @redis/client`) before using this helper or the redis cache / rate-limiter driver. A project on the default in-memory cache never loads it.

:::

```ts
import { getRedisClient, getRedisClientSync } from '@adaptivestone/framework/helpers/redis/redisConnection.js';
const redisClient = await getRedisClient();
const redisClientSync = await getRedisClientSync();
```

The only difference is that `getRedisClientSync` returns the Redis client immediately and establishes the connection in the background.

## Validation schema (`defineSchema`)

A zero-dependency way to build a [Standard Schema](https://standardschema.dev/) validator from a plain function — for simple `request:` / `query:` schemas, without pulling in a validator library. The framework itself uses it, so it ships validator-free.

```ts
import { defineSchema } from "@adaptivestone/framework/services/validate/defineSchema.js";

const loginSchema = defineSchema<{ email: string }>((value) => {
  const v = (value ?? {}) as Record<string, unknown>;
  if (typeof v.email !== "string" || !v.email.includes("@")) {
    return { issues: [{ message: "validation.email", path: ["email"] }] };
  }
  // Return only known keys — unknown input is stripped by construction.
  return { value: { email: v.email } };
});
```

The `Output` generic (`{ email: string }`) feeds the typed handler signature — codegen reads `StandardSchemaV1.InferOutput`. Return `{ value }` on success or `{ issues }` on failure; each message is an i18n key or literal text. For richer or deeply-nested validation, bring zod / valibot / arktype / yup as the route schema instead — `defineSchema` has no combinators on purpose. See [Routes → Validation](./06-Controllers/02-routes.md#validation).

## Uploaded-file type (`File`)

A vendor-neutral type for files uploaded via `multipart/form-data`. It aliases the parser's file class today and re-points at the web-standard `File` after the transport-neutral parser swap, so your validation code stays stable across that change.

```ts
import { File } from "@adaptivestone/framework/types.js";
import { z } from "zod";

request: z.object({
  avatar: z.array(z.instanceof(File)).length(1).transform(([f]) => f), // one file  -> File
  avatars: z.array(z.instanceof(File)),                                // many files -> File[]
});
```

`File` is exported as both a value (for `instanceof`) and a type. Every multipart field arrives as an array, so declare cardinality with your validator's array support. See [Routes → File Validation](./06-Controllers/02-routes.md#file-validation).

:::warning Deprecated: `YupFile`
The older yup-specific `YupFile` helper (`@adaptivestone/framework/helpers/yup.js`) is **deprecated and will be removed in v6** — it now emits a runtime `DeprecationWarning` (escalate it to a thrown error with Node's `--throw-deprecation`). Migrate to the `File` export above; it works with any validator and needs no yup.
:::


---


# Document 20: 15-recipes

<!-- Source: 15-recipes.md -->

# Recipes

A task-oriented cookbook for the things you build most often. Each recipe is a minimal, copy-paste starting point — follow the links into [Controllers](06-Controllers/01-intro.md) and [Routes](06-Controllers/02-routes.md) for the full reference.

:::note
Framework **package** imports use the published `.js` extension (the package ships compiled `.js`). Your own project files can be `.ts`.
:::

## Where the handler types come from

The recipes below type handlers with two things — here's where each comes from:

- **`FrameworkRequest`** — imported from `@adaptivestone/framework/services/http/HttpServer.js`. It's the Express `Request` plus the base `req.appInfo` (`app`, `ip`, `request`, `query`, `i18n`). Use it for handlers that only read `req.params` / `req.body` and don't need validated input or middleware-provided fields. `Response` and `NextFunction` come from `express`.
- **Generated `<Method>Request` aliases** — emitted by `npm run gen` into `<Controller>.routes.gen.ts`, one per handler method (`postCreate` → `PostCreateRequest`). Each one extends the base context with what *that route* actually carries: typed `req.params`, your `request:` / `query:` schema output on `req.appInfo`, and every field the route's middleware chain declares via `static get provides()` (`req.appInfo.user`, `req.appInfo.pagination`, …).

Rule of thumb: if the handler reads `req.appInfo.request` / `.query` / `.user` / `.pagination` or precise `req.params`, use the generated alias; otherwise `FrameworkRequest` is enough. See [Typed handler signatures](06-Controllers/02-routes.md#typed-handler-signatures-codegen).

## Add a controller

Drop a file in `src/controllers/`. The filename becomes the route prefix (`Article.ts` → `/article`), and every method listed in `routes` becomes an endpoint. No registration step — the framework auto-loads the file.

```ts
// src/controllers/Article.ts
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
import type { Response } from "express";

class Article extends AbstractController {
  get routes() {
    return {
      get: {
        "/": { handler: this.list },
        "/:id": { handler: this.getOne },
      },
    };
  }

  async list(req: FrameworkRequest, res: Response) {
    const Article = this.app.getModel("Article");
    return res.status(200).json({ data: await Article.find() });
  }

  async getOne(req: FrameworkRequest, res: Response) {
    const Article = this.app.getModel("Article");
    return res.status(200).json({ data: await Article.findById(req.params.id) });
  }
}

export default Article;
```

These handlers read only `req.params` and the model, so the base `FrameworkRequest` is enough. To customize the prefix, override `getHttpPath()`. See [Controllers](06-Controllers/01-intro.md).

:::warning
`req.params.id` is **not** validated by the framework. Passing a malformed id to `findById` throws a Mongoose `CastError` (a 500). Guard it — see [Validate an ObjectId](#validate-an-objectid).
:::

## Add a route with a body schema

Declare a `request:` schema inline. The framework validates and casts the body **before** your handler runs, strips unknown keys, and exposes the typed result on `req.appInfo.request`. Run `npm run gen` to emit `Article.routes.gen.ts` (where `PostCreateRequest` lives) and type the handler with it.

```ts
// src/controllers/Article.ts
import type { PostCreateRequest } from "./Article.routes.gen.ts";
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
import type { Response } from "express";
import { object, string } from "yup";

class Article extends AbstractController {
  get routes() {
    return {
      post: {
        "/": {
          handler: this.postCreate,
          request: object().shape({
            title: string().trim().min(3).max(300).required(),
            body: string().trim().required(),
          }),
        },
      },
    };
  }

  async postCreate(req: PostCreateRequest, res: Response) {
    // req.appInfo.request.title / .body are typed (from the schema) and already validated
    const Article = this.app.getModel("Article");
    const created = await Article.create(req.appInfo.request);
    return res.status(201).json({ data: created });
  }
}
```

The handler type `PostCreateRequest` is generated from the method name (`postCreate` → `PostCreateRequest`). Any [Standard Schema](https://standardschema.dev/) validator works (yup, zod, valibot, arktype), or the zero-dependency [`defineSchema`](14-helpers.md). See [Routes → Validation](06-Controllers/02-routes.md#validation).

## Validate an ObjectId

The framework validates `request:` (body) and `query:` schemas before your handler runs — but **not** path params. A handler that passes a raw `:id` straight to Mongoose (`Model.findById(req.params.id)`) throws a Mongoose **`CastError`** on a malformed id, and a `CastError` is not a `ValidationError`, so it surfaces as a **500**, not a clean 400. Validate ids explicitly.

**Body / query fields** — add the check to the schema you already declare. A strict 24-hex pattern is enough:

```ts
import { object, string } from "yup";

// in a route:
query: object().shape({
  authorId: string().matches(/^[0-9a-fA-F]{24}$/, "must be a valid id"),
}),
// zod equivalent: z.object({ authorId: z.string().regex(/^[0-9a-fA-F]{24}$/) })
```

A bad value is rejected with the framework's standard `{ errors: { authorId: [...] } }` 400 — it never reaches your handler.

**Path params (`:id`)** — params aren't schema-validated, so guard them in the handler before touching the model:

```ts
import mongoose from "mongoose";
import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
import type { Response } from "express";

async getOne(req: FrameworkRequest, res: Response) {
  if (!mongoose.isValidObjectId(req.params.id)) {
    // Match the framework's validation-error shape: { errors: { field: [msg] } }
    return res.status(400).json({ errors: { id: ["must be a valid id"] } });
  }
  const Article = this.app.getModel("Article");
  return res.status(200).json({ data: await Article.findById(req.params.id) });
}
```

:::note Strict vs loose
`mongoose.isValidObjectId` is lenient — it also accepts any 12-character string (and some numbers). For a tighter check, use the 24-hex regex (`/^[0-9a-fA-F]{24}$/`).
:::

For many `:id` routes, factor the guard into a small reusable middleware (see [Write a middleware that contributes to `req.appInfo`](#write-a-middleware-that-contributes-to-reqappinfo)) and just list it in each route's `middleware`.

:::note Planned
A declarative `params:` route schema — validating and coercing path params the same way `request:` / `query:` do, with the typed result on `req.appInfo.params` — is planned for a future release. Until then, validate params in the handler (or a middleware) as shown above.
:::

## Wire pagination

Add the built-in `Pagination` middleware to the route. It reads `page` / `limit` from the query and puts `{ page, limit, skip }` on `req.appInfo.pagination`. Because that field is middleware-provided, type the handler with its generated alias (`ListRequest`) — `Pagination`'s `provides` flows into it.

```ts
import Pagination from "@adaptivestone/framework/services/http/middleware/Pagination.js";
import type { ListRequest } from "./Article.routes.gen.ts";
import type { Response } from "express";

get routes() {
  return {
    get: {
      "/": {
        handler: this.list,
        middleware: [[Pagination, { limit: 20, maxLimit: 100 }]],
      },
    },
  };
}

async list(req: ListRequest, res: Response) {
  const { skip, limit, page } = req.appInfo.pagination; // typed via Pagination's `provides`
  const Article = this.app.getModel("Article");
  const [items, total] = await Promise.all([
    Article.find().skip(skip).limit(limit),
    Article.countDocuments(),
  ]);
  return res.status(200).json({ data: { items, page, limit, total } });
}
```

`[Pagination, { limit, maxLimit }]` is the tuple form — a middleware class plus its params. See [Middleware → Pagination](06-Controllers/03-middleware.md#pagination).

## Require authentication

Put `GetUserByToken` (parses the token → `req.appInfo.user`) and `Auth` (rejects unauthenticated requests) in the chain. After `Auth`, `req.appInfo.user` is typed as required in the generated alias — no `if (!user)` guard needed.

```ts
import GetUserByToken from "@adaptivestone/framework/services/http/middleware/GetUserByToken.js";
import Auth from "@adaptivestone/framework/services/http/middleware/Auth.js";
import type { ListMineRequest } from "./Article.routes.gen.ts";
import type { Response } from "express";

get routes() {
  return {
    get: {
      "/mine": {
        handler: this.listMine,
        middleware: [GetUserByToken, Auth],
      },
    },
  };
}

async listMine(req: ListMineRequest, res: Response) {
  // req.appInfo.user is required here (Auth's `provides`)
  const Article = this.app.getModel("Article");
  return res.status(200).json({
    data: await Article.find({ owner: req.appInfo.user._id }),
  });
}
```

To apply one chain to **every** route in the controller, use the middleware Map instead:

```ts
static get middleware() {
  return new Map([["/{*splat}", [GetUserByToken, Auth]]]);
}
```

See [Middleware](06-Controllers/03-middleware.md).

## Write a middleware that contributes to `req.appInfo`

Extend `AbstractMiddleware`, set your field on `req.appInfo`, and declare `static get provides()` so handlers downstream get it typed. Type the middleware's own `req` with the field it writes (an inline intersection on the base `FrameworkRequest`).

```ts
// src/middleware/WithArticleCount.ts
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";
import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
import type { Response, NextFunction } from "express";

class WithArticleCount extends AbstractMiddleware {
  static get description() {
    return "Adds the total article count to req.appInfo";
  }

  static get provides() {
    return {} as { articleCount: number };
  }

  async middleware(
    req: FrameworkRequest & { appInfo: { articleCount?: number } },
    res: Response,
    next: NextFunction,
  ) {
    const Article = this.app.getModel("Article");
    req.appInfo.articleCount = await Article.countDocuments();
    return next();
  }
}

export default WithArticleCount;
```

The object `provides` returns is always `{}` — only its cast type matters (codegen reads it; the runtime ignores it). Any route with `WithArticleCount` in its chain now has `req.appInfo.articleCount: number` in its generated alias. See [Routes → Middleware-provided types](06-Controllers/02-routes.md#middleware-provided-types).

## Override a framework controller

Create a controller with the same filename as a built-in one and extend it — your version wins via the [file-inheritance](03-files-inheritance.md) mechanism. Override only the handlers you want to change.

```ts
// src/controllers/Auth.ts
import OriginalAuth from "@adaptivestone/framework/controllers/Auth.js";
import type { FrameworkRequest } from "@adaptivestone/framework/services/http/HttpServer.js";
import type { Response } from "express";

class Auth extends OriginalAuth {
  async postLogin(req: FrameworkRequest, res: Response) {
    // your custom login — call super for the default behavior, or replace it
    return super.postLogin(req, res);
  }
}

export default Auth;
```

The same approach works for models and configs. See [File Inheritance](03-files-inheritance.md).

## Test a controller with the framework helpers

The framework boots a real test server and an in-memory Mongo. Use `getTestServerURL()` for the base URL and `appInstance.getModel()` to seed and reset data.

```ts
// src/controllers/Article.test.ts
import { appInstance } from "@adaptivestone/framework/helpers/appInstance.js";
import { getTestServerURL } from "@adaptivestone/framework/tests/testHelpers.js";
import { beforeEach, describe, expect, it } from "vitest";

describe("Article controller", () => {
  beforeEach(async () => {
    await appInstance.getModel("Article").deleteMany({});
  });

  it("creates an article", async () => {
    const res = await fetch(getTestServerURL("/article"), {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title: "Hello world", body: "..." }),
    });
    expect(res.status).toBe(201);
  });
});
```

For authenticated requests, create a user and send its token in the `Authorization` header. See the [Testing](09-testsing.md) chapter for the full setup, helpers, and CI examples.


---


# Document 21: 16-anti-patterns

<!-- Source: 16-anti-patterns.md -->

# Anti-patterns

Common mistakes when working with the framework, and what to do instead. Most of them map to a feature that already solves the problem — reach for the seam, not the workaround.

## Don't hand-edit `.routes.gen.ts`

❌ Editing a generated `<File>.routes.gen.ts` file.
✅ Edit the `routes` getter / `request:` schema, then run `npm run gen`.

Gen files are overwritten on every `npm run gen` (and every `check:types`) — your edits vanish. They're derived from the `routes` getter and the resolved middleware chain, and they're gitignored for that reason. Change the source and regenerate. See [When to run codegen](10-cli.md#when-to-run-codegen).

## Don't `new` a middleware yourself

❌ `middleware: [new Pagination(app, { limit: 20 })]`
✅ `middleware: [[Pagination, { limit: 20 }]]`

Declare middleware as a class, or a `[Class, params]` tuple. The framework instantiates and caches them for you, and reads their static metadata (`provides`, schemas) **without** constructing them. A manual `new` runs constructor side effects at the wrong time and bypasses that caching. See [Middleware](06-Controllers/03-middleware.md).

## Don't mutate `req.body` to "validate"

❌ Reading and normalizing `req.body` by hand inside the handler.
✅ Declare a `request:` (or `query:`) schema; read the validated, cast result from `req.appInfo.request`.

The route schemas validate and coerce before the handler runs, produce a typed `req.appInfo.request` / `req.appInfo.query`, strip unknown keys, and feed codegen (and later OpenAPI). Hand-rolled parsing in the handler gets none of that. See [Validation](06-Controllers/02-routes.md#validation).

## Don't put state in the `routes` getter

❌ A `routes` getter that reads `this.something` assigned in the constructor.
✅ Keep `routes` declarative — list handler methods and inline schemas only.

`npm run gen` reads `routes` **statically from the source AST** — it never imports or constructs your controller, so type generation stays free of constructor side effects. The cost: if the `routes` getter isn't a plain object literal (it reads `this.something`, loops, or otherwise computes the route shape), codegen can't analyze it and **the run fails** — there is no constructor fallback. Move the dynamic part into handlers or a module-level constant so the returned shape stays literal. See [Keep `routes` declarative](06-Controllers/02-routes.md#keep-routes-declarative-codegen-reads-the-source-never-your-constructor).

## Don't reach into raw Express when `req.appInfo` already has it

❌ Re-parsing cookies, re-loading the user, or recomputing pagination in every handler.
✅ Read what middleware already put on `req.appInfo` — `user`, `request`, `query`, `pagination`, `i18n`, plus any field your own middleware declares via `provides`.

`req.appInfo` is the framework's typed request context. Middleware populates it once; handlers and codegen consume it. Reaching around it duplicates work and loses the types. See [Middleware-provided types](06-Controllers/02-routes.md#middleware-provided-types).

## Don't monkey-patch the framework — report it

❌ Copy-pasting a framework file to tweak it, overriding internals to route around a bug, or shimming a private method.
✅ Open an issue at [github.com/adaptivestone/framework/issues](https://github.com/adaptivestone/framework/issues) with a minimal repro.

Local workarounds rot, hide the bug from everyone else, and break on the next update. The framework is built to be extended through supported seams — [file inheritance](03-files-inheritance.md), middleware, `provides`, and config — not patched. If the seam you need is missing, that's worth an issue too.


---


# Document 22: 17-openapi

<!-- Source: 17-openapi.md -->

# OpenAPI

The framework generates an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0/) document from your controllers. Paths, parameters, request bodies, security, and tags are all derived from the route definitions you already write — point Swagger UI, Redoc, or a client-SDK generator at the output.

## Generate

```bash
node src/cli.ts openapi                       # print to stdout
node src/cli.ts openapi --output openapi.json # write to a file
```

Add a script to `package.json`:

```json
"openapi": "node src/cli.ts openapi"
```

```bash
npm run openapi -- --output openapi.json
```

:::note
The command loads your controllers but **opens no database or network connection and binds no port** — it walks the route registry and reads your schemas in-process. It's safe to run in CI. (Unlike codegen, which is on your hot path, this is a cold, occasional command, so it loads the real controller instances to read live schema objects.)
:::

## What gets documented

Everything comes from the route definitions you already have — there is nothing OpenAPI-specific to maintain separately:

| OpenAPI field | Source |
|---|---|
| `operationId` | handler method name |
| `tags` | controller class name |
| `summary` | route [`description`](06-Controllers/02-routes.md#route-third-level-route-object-level) |
| path `parameters` | `:name` path segments |
| query `parameters` | route [`query:`](06-Controllers/02-routes.md#query) schema (+ middleware query schemas) |
| `requestBody` | route [`request:`](06-Controllers/02-routes.md#request) schema or [content-type map](06-Controllers/02-routes.md#different-schemas-per-content-type) (+ middleware request schemas) |
| `security` | middleware [`static get usedAuthParameters()`](#documenting-auth-security-schemes) |
| `info` / `servers` | your `package.json` + the `http` config (`port`, `myDomain`) |

Output is **OpenAPI 3.1** (JSON Schema 2020-12) only.

## Request bodies come from your schemas

Body and query shapes are produced by introspecting the **same validation schemas** you already use at runtime — through the validator driver's `toJsonSchema`. How much detail you get depends on the validator:

| Validator | OpenAPI body |
|---|---|
| [Zod](https://zod.dev/) | full JSON Schema via native `z.toJSONSchema` — types, formats, `min`/`max`, patterns, enums, defaults |
| [Yup](https://github.com/jquense/yup) | JSON Schema from `.describe()` — types, `required`, enums, nullable, arrays, `date` → `date-time` |
| [ArkType](https://arktype.io/) / any schema exposing a `.toJsonSchema()` method | its native output |
| [`defineSchema`](06-Controllers/02-routes.md#zero-dependency-schemas-defineschema) / a hand-rolled `~standard` function | **not introspectable** — a placeholder schema + a warning |

:::note Use a declarative schema if you want a documented body
`defineSchema` and custom `~standard` functions are *imperative* — they validate but expose no shape, so the generator can't describe them and emits a placeholder object instead. If an endpoint's body should appear in the spec, declare it with Zod or Yup. The command prints a warning listing every schema it couldn't introspect, e.g.:

```
OpenAPI: 1 schema(s) could not be fully introspected:
  POST /auth/login body: schema introspection unavailable — placeholder emitted.
```
:::

This is why the generator must load your controllers at runtime rather than read the generated types: JSON Schema can only be produced from the live schema object (`z.toJSONSchema(...)`, `schema.describe()`), not from a TypeScript type.

## Documenting auth (security schemes)

A middleware advertises the security scheme(s) it enforces with a `static get usedAuthParameters()` getter. The generator reads it **off the class — no instantiation** — adds each entry to `components.securitySchemes`, and attaches a `security` requirement to every operation whose middleware chain includes that middleware.

```ts
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";

class TokenAuth extends AbstractMiddleware {
  static get usedAuthParameters() {
    return [
      // http bearer scheme
      { name: "bearerAuth", type: "http", scheme: "bearer", description: "Bearer token" },
      // or an apiKey header
      { name: "X-Api-Key", type: "apiKey", in: "header", description: "API key" },
    ];
  }

  async middleware(req, res, next) {
    // ... runtime auth logic ...
  }
}
```

| Field | Meaning |
|---|---|
| `name` | scheme key in `components.securitySchemes` (for `apiKey`, also the header/query name) |
| `type` | `'apiKey'` or `'http'` |
| `in` | for `apiKey`: `'header'` (default) / `'query'` / `'cookie'` |
| `scheme` | for `http`: `'bearer'`, `'basic'`, … |
| `description` | shown in the docs UI |

The built-in [`GetUserByToken`](06-Controllers/03-middleware.md) already declares its `Authorization` header + bearer schemes, so any route behind it is documented as secured automatically.

## Current limitations

- **Response bodies are not yet schema-documented.** Every operation carries a generic `200`/`400`/`401`/`404` response with a text description but no body schema. (Documenting response shapes is on the roadmap — it needs a declared `response:` schema, since a response has no runtime schema object to introspect.)
- **Catch-all (`{*splat}`) routes** are approximated as a single `{splat}` path parameter, because OpenAPI has no catch-all; the command warns when it does this.
- The route `description` becomes the operation `summary`; there is no separate long description, `deprecated` flag, or per-tag description yet.


---


<!-- End of Documentation -->
