{"id":23468816,"url":"https://github.com/etienne-bechara/crux","last_synced_at":"2025-08-19T09:10:33.710Z","repository":{"id":52494744,"uuid":"309495806","full_name":"etienne-bechara/crux","owner":"etienne-bechara","description":"Node.js backend package including: framework (NestJS), HTTP server (Fastify), HTTP client (Fetch), distributed caching (ioredis), ORM (MikroORM), swagger documentation (Redoc), logger (Loki), metrics (Prometheus) and tracing (Tempo with OpenTelemetry).","archived":false,"fork":false,"pushed_at":"2025-07-04T14:58:29.000Z","size":3600,"stargazers_count":14,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-08-18T07:42:47.775Z","etag":null,"topics":["backend","cache","fastify","framework","ioredis","logs","loki","metrics","mikro-orm","nestjs","nodejs","openapi","opentelemetry","orm","prometheus","redoc","swagger","tempo","tracing"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/etienne-bechara.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-11-02T21:09:57.000Z","updated_at":"2025-07-04T14:58:32.000Z","dependencies_parsed_at":"2023-07-16T17:13:40.039Z","dependency_job_id":"a5ba675e-ea75-44d9-a688-3d1181ac6c6a","html_url":"https://github.com/etienne-bechara/crux","commit_stats":{"total_commits":864,"total_committers":2,"mean_commits":432.0,"dds":"0.32291666666666663","last_synced_commit":"a6cedf2e14e20073764c9d07c6b595dd3aa0238a"},"previous_names":["etienne-bechara/nestjs-core"],"tags_count":335,"template":false,"template_full_name":null,"purl":"pkg:github/etienne-bechara/crux","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/etienne-bechara%2Fcrux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/etienne-bechara%2Fcrux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/etienne-bechara%2Fcrux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/etienne-bechara%2Fcrux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/etienne-bechara","download_url":"https://codeload.github.com/etienne-bechara/crux/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/etienne-bechara%2Fcrux/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270962297,"owners_count":24675952,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-18T02:00:08.743Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend","cache","fastify","framework","ioredis","logs","loki","metrics","mikro-orm","nestjs","nodejs","openapi","opentelemetry","orm","prometheus","redoc","swagger","tempo","tracing"],"created_at":"2024-12-24T14:32:16.412Z","updated_at":"2025-08-19T09:10:33.702Z","avatar_url":"https://github.com/etienne-bechara.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CRUX\n\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=crux\u0026metric=alert_status)](https://sonarcloud.io/summary/new_code?id=crux)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=crux\u0026metric=coverage)](https://sonarcloud.io/summary/new_code?id=crux)\n[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=crux\u0026metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=crux)\n[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=crux\u0026metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=crux)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=crux\u0026metric=security_rating)](https://sonarcloud.io/summary/new_code?id=crux)\n\nCRUX is an opinionated Node.js framework package designed for backend projects. It integrates a range of libraries and patterns commonly used in distributed, stateless applications that require database access.\n\n- **Framework:** [NestJS](https://docs.nestjs.com/)\n- **HTTP Server:** [Fastify](https://www.fastify.io/docs/latest/)\n- **HTTP Client:** [Fetch](https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#fetch)\n- **Caching:** [ioredis](https://www.npmjs.com/package/ioredis) (distributed) or in-memory (local)\n- **ORM:** [MikroORM](https://mikro-orm.io/docs/installation)\n- **OpenAPI:** [Scalar](https://scalar.com/)\n- **Logs:** [Loki](https://grafana.com/docs/loki/latest/api/)\n- **Metrics:** [Prometheus](https://github.com/siimon/prom-client)\n- **Tracing:** [Tempo](https://grafana.com/docs/tempo/latest/api_docs/) with [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-js)\n\n---\n\n## Disclaimer\n\nThis framework was created to avoid rebuilding similar boilerplates for distributed, stateless backend projects with database access. It is highly opinionated and should be treated more as a reference for creating your own solutions rather than as a production-ready product.\n\n---\n\n## Installation\n\n1. **Create and initialize a new Node.js project**, then install TypeScript, its types, a live-reload tool, and this package.  \n\n   We recommend using [pnpm](https://pnpm.io/) as your package manager, and [ts-node-dev](https://github.com/wclr/ts-node-dev) for live reloading:\n\n   ```sh\n   mkdir my-project\n   cd my-project\n\n   git init\n   npm init -y\n\n   npm i -g pnpm\n   pnpm i -D typescript @types/node ts-node-dev\n   pnpm i -E @bechara/crux\n\n   tsc --init\n   ```\n\n2. **Create a `main.ts` file** in a `/source` folder with the following content:\n\n   ```ts\n   // /source/main.ts\n   import { AppModule } from '@bechara/crux';\n\n   void AppModule.boot();\n   ```\n\n3. **Add a `dev` script** in your `package.json`:\n\n   ```json\n   {\n     \"scripts\": {\n       \"dev\": \"tsnd --exit-child --rs --watch *.env --inspect=0.0.0.0:9229 ./source/main.ts\"\n     }\n   }\n   ```\n\n4. **Start the application**:\n\n   ```sh\n   pnpm dev\n   ```\n\n   You can test it by sending a request to `GET /`. You should receive a successful response with a `204` status code.\n\n---\n\n## Development\n\nUsing this framework mostly follows the official [NestJS Documentation](https://docs.nestjs.com/). Familiarize yourself with the following core NestJS concepts before continuing:\n\n- [Modules](https://docs.nestjs.com/modules)\n- [Controllers](https://docs.nestjs.com/controllers)\n- [Providers](https://docs.nestjs.com/providers)\n\n### Key Differences\n\n1. **Imports from `@bechara/crux`**  \n   All NestJS imports, such as `@nestjs/common` or `@nestjs/core`, are re-exported by `@bechara/crux`.  \n   Instead of:\n   ```ts\n   import { Injectable } from '@nestjs/common';\n   ```\n   use:\n   ```ts\n   import { Injectable } from '@bechara/crux';\n   ```\n\n2. **Automatic Module Loading**  \n   Any file ending with `*.module.ts` in your source folder is automatically loaded by `main.ts`. You don’t need to create a global module importing them manually.  \n   Instead of:\n   ```ts\n   @Global()\n   @Module({\n     imports: [\n       FooModule,\n       BarModule,\n       BazModule,\n     ],\n   })\n   export class AppModule { }\n   ```\n   simply do:\n   ```ts\n   import { AppModule } from '@bechara/crux';\n\n   void AppModule.boot();\n   // FooModule, BarModule, and BazModule are automatically loaded\n   // as long as they're in the source folder and named *.module.ts\n   ```\n\n---\n\n## Testing\n\nTesting can involve multiple environment variables, making it more complex to write boilerplate code. For this reason, `AppModule` offers a built-in `compile()` method to create an application instance without serving it.\n\n### Usage\n\nIn your `*.service.spec.ts`, add a `beforeAll()` hook to compile an application instance:\n\n```ts\nimport { AppModule } from '@bechara/crux';\n\ndescribe('FooService', () =\u003e {\n  let fooService: FooService;\n\n  beforeAll(async () =\u003e {\n    const app = await AppModule.compile();\n    fooService = app.get(FooService);\n  });\n\n  describe('readById', () =\u003e {\n    it('should read a foo entity', async () =\u003e {\n      const foo = await fooService.readById(1);\n      expect(foo).toEqual({ name: 'bob' });\n    });\n  });\n});\n```\n\nIf you need custom options, the `compile()` method supports the same boot options as `boot()`.\n\nRun all tests with:\n\n```sh\npnpm test\n```\n\nOr a specific set:\n\n```sh\npnpm test -- foo\n```\n\n---\n\n# Curated Modules\n\nBelow are details about the main modules in this framework and how to use them.\n\n## Application Module\n\nActs as the entry point, wrapping other modules in this package and automatically loading any `*.module.ts` in your source folder.  \n\nBy default, it serves an HTTP adapter using [Fastify](https://www.fastify.io/). The following custom enhancers are globally applied:\n\n- [app.interceptor.ts](source/app/app.interceptor.ts): A timeout interceptor that cancels requests exceeding the configured runtime.\n- [app.filter.ts](source/app/app.filter.ts): An exception filter integrated with the logging service for standardized error output.\n- [ClassSerializer](https://docs.nestjs.com/techniques/serialization#serialization) for response serialization.\n- [ValidationPipe](https://docs.nestjs.com/techniques/validation#validation) for DTO validation and transformation.\n\n### Environment Configuration\n\n| Variable  | Mandatory | Type                                                   |\n|-----------|:--------:|--------------------------------------------------------|\n| NODE_ENV  | Yes       | [AppEnvironment](source/app/app.enum/app.environment.ts) |\n\n### Module Options\n\nWhen booting your application, you can configure options as described in [AppBootOptions](source/app/app.interface.ts):\n\n```ts\nimport { AppModule } from '@bechara/crux';\n\nvoid AppModule.boot({\n  // See AppBootOptions for detailed properties\n});\n```\n\nProvided options will be merged with the [default configuration](source/app/app.config.ts).\n\n---\n\n## Configuration Module\n\nAllows asynchronous population of secrets through `*.config.ts` files containing configuration classes.  \n\nDecorate a class with `@Config()` to make it available as a regular NestJS provider. Any property decorated with `@InjectSecret()` will have its value extracted from `process.env` and injected into the class.\n\n### Usage\n\n1. **Create a `*.config.ts` file** with a class decorated by `@Config()`.  \n2. Decorate any properties with `@InjectSecret()`.  \n3. Optionally, apply `class-validator` and `class-transformer` decorators for validation and transformation.\n\nExample:\n\n```ts\nimport { Config, InjectSecret, IsUrl, IsString, Length, ToNumber } from '@bechara/crux';\n\n@Config()\nexport class FooConfig {\n  @InjectSecret()\n  @IsUrl()\n  FOO_API_URL: string;\n\n  @InjectSecret({ key: 'foo_authorization' })\n  @IsString() @Length(36)\n  FOO_API_KEY: string;\n\n  @InjectSecret({ fallback: '15' })\n  @ToNumber()\n  FOO_API_MAX_CONCURRENCY: number;\n}\n```\n\nUse the configuration in your module and services:\n\n```ts\n@Injectable()\nexport class FooService {\n  constructor(private readonly fooConfig: FooConfig) {}\n\n  public async readFooById(id: number) {\n    console.log(this.fooConfig.FOO_API_MAX_CONCURRENCY);\n    // ...\n  }\n}\n```\n\n---\n\n## Context Module\n\nProvides `ContextService`, an alternative to `REQUEST`-scoped injections in NestJS. It leverages Node.js AsyncLocalStorage to store request data without the performance or dependency-resolution challenges of `REQUEST` scope.\n\n### Usage\n\n```ts\nimport { ContextService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(private readonly contextService: ContextService) {}\n\n  public getRequestAuthorization() {\n    const req = this.contextService.getRequest();\n    return req.headers.authorization;\n  }\n\n  public getUserId() {\n    return this.contextService.getMetadata('userId');\n  }\n\n  public setUserId(userId: string) {\n    this.contextService.setMetadata('userId', userId);\n  }\n}\n```\n\n---\n\n## Documentation Module\n\nGenerates OpenAPI documentation using [NestJS OpenAPI Decorators](https://docs.nestjs.com/openapi/decorators).  \n\n- **User interface:** available at `/docs`\n- **OpenAPI spec:** available at `/docs/json`\n\n---\n\n## Http Module\n\nProvides a wrapper over Node.js Fetch API, exposing methods to make HTTP requests. Its scope is transient: every injection yields a fresh instance.\n\n### Basic Usage\n\nIn your module:\n\n```ts\nimport { HttpModule } from '@bechara/crux';\n\n@Module({\n  imports: [HttpModule.register()],\n  controllers: [FooController],\n  providers: [FooService],\n})\nexport class FooModule {}\n```\n\nIn your service:\n\n```ts\nimport { HttpService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(private readonly httpService: HttpService) {}\n\n  public async readFooById(id: number) {\n    return this.httpService.get('https://foo.com/foo/:id', {\n      replacements: { id },\n    });\n  }\n}\n```\n\n### Async Registration\n\nTo configure base parameters (host, headers, API keys, etc.) using environment secrets:\n\n```ts\nimport { HttpAsyncModuleOptions, HttpModule } from '@bechara/crux';\n\nconst httpModuleOptions: HttpAsyncModuleOptions = {\n  inject: [FooConfig],\n  useFactory: (fooConfig: FooConfig) =\u003e ({\n    prefixUrl: fooConfig.FOO_API_URL,\n    headers: { authorization: fooConfig.FOO_API_KEY },\n    timeout: 20_000,\n  }),\n};\n\n@Module({\n  imports: [HttpModule.registerAsync(httpModuleOptions)],\n  controllers: [FooController],\n  providers: [FooConfig, FooService],\n  exports: [FooConfig, FooService],\n})\nexport class FooModule {}\n```\n\n---\n\n## Cache Module\n\nAllows caching of inbound responses for controller paths decorated with `@Cache()`. Uses Redis (through `ioredis`) if available, falling back to an in-memory store.\n\n### Usage\n\n```ts\nimport { Cache, Controller, Get, Param } from '@bechara/crux';\n\n@Controller('foo')\nexport class FooController {\n  constructor(private readonly fooService: FooService) {}\n\n  @Cache({ ttl: 60_000 }) // 60 seconds\n  @Get(':id')\n  public getFoo(@Param('id') id: string): Promise\u003cFoo\u003e {\n    return this.fooService.getFooById(id);\n  }\n}\n```\n\n### Environment Variables\n\n| Variable       | Required | Type   | Default |\n|----------------|:--------:|:------:|:-------:|\n| CACHE_HOST     | No       | string |         |\n| CACHE_PORT     | No       | number |         |\n| CACHE_USERNAME | No       | string |         |\n| CACHE_PASSWORD | No       | string |         |\n\n---\n\n## Redis Module\n\nProvides a connection to Redis via [ioredis](https://github.com/redis/ioredis). It automatically uses credentials configured through the Cache Module.\n\n### Usage\n\n```ts\nimport { RedisService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(private readonly redisService: RedisService) {}\n\n  public getFoo() {\n    const foo = this.redisService.get('FOO');\n    if (!foo) {\n      throw new InternalServerErrorException('Foo not available');\n    }\n    return foo;\n  }\n\n  public setFoo(params: unknown) {\n    const ttl = 5 * 60_000; // 5 minutes\n    this.redisService.set('FOO', params, { ttl });\n  }\n}\n```\n\n---\n\n## Memory Module\n\nOffers a simple in-memory key-value store with support for TTL.\n\n### Usage\n\n```ts\nimport { MemoryService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(private readonly memoryService: MemoryService) {}\n\n  public getFoo() {\n    const foo = this.memoryService.get('FOO');\n    if (!foo) {\n      throw new InternalServerErrorException('Foo not available');\n    }\n    return foo;\n  }\n\n  public setFoo(params: unknown) {\n    const ttl = 5 * 60_000; // 5 minutes\n    this.memoryService.set('FOO', params, { ttl });\n  }\n}\n```\n\n---\n\n## Promise Module\n\nProvides utility functions for working with Promises (retrying, deduplication, throttling, etc.). Refer to [PromiseService](source/promise/promise.service.ts) for details.\n\n### Usage\n\n```ts\nimport { PromiseService, HttpService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(\n    private readonly promiseService: PromiseService,\n    private readonly httpService: HttpService,\n  ) {}\n\n  public async readFooOrTimeout(): Promise\u003cunknown\u003e {\n    const timeout = 5000; // 5 seconds\n    return this.promiseService.resolveOrTimeout({\n      promise: () =\u003e this.httpService.get('foo'),\n      timeout,\n    });\n  }\n\n  public async readFooWithRetry(): Promise\u003cunknown\u003e {\n    return this.promiseService.retryOnRejection({\n      method: () =\u003e this.httpService.get('foo'),\n      retries: 5,\n      timeout: 120_000, // 2 minutes\n      delay: 500,       // 500 ms\n    });\n  }\n}\n```\n\n---\n\n## Metric Module\n\nCollects metrics using [Prometheus](https://prometheus.io/). Metrics can be scraped at the `/metrics` endpoint.\n\n### Usage\n\nInject `MetricService` to create custom counters, gauges, histograms, or summaries:\n\n```ts\nimport { Histogram, MetricService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(private readonly metricService: MetricService) {\n    this.setupMetrics();\n  }\n\n  private setupMetrics(): void {\n    this.metricService.getHistogram('foo_size', {\n      help: 'Size of foo.',\n      labelNames: ['foo', 'bar'],\n      buckets: [1, 3, 5, 8, 13],\n    });\n  }\n\n  public readFoo() {\n    const histogram = this.metricService.getHistogram('foo_size');\n    // ...\n  }\n}\n```\n\n---\n\n## Log Module\n\nProvides a logging service with predefined severity levels. Messages are broadcast to all configured transports, which decide whether to publish them based on their own configuration.\n\n### Usage\n\n```ts\nimport { LogService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(\n    private readonly fooRepository: FooRepository,\n    private readonly logService: LogService,\n  ) {}\n\n  public async readFooById(id: number) {\n    this.logService.debug(`Reading foo with ID ${id}`);\n\n    try {\n      const foo = await this.fooRepository.readById(id);\n      this.logService.notice(`Successfully read foo with ID ${id}`);\n      return foo;\n    } catch (error) {\n      this.logService.error(`Failed to read foo`, error, { id });\n      throw new InternalServerErrorException();\n    }\n  }\n}\n```\n\n### Call Signatures\n\nLogging methods accept any combination of strings, `Error` objects, or plain objects:\n\n```ts\nthis.logService.error('Something went wrong');\nthis.logService.error('Something went wrong', new Error('Log example'));\nthis.logService.error(new Error('Log example'), { key: 'value' });\nthis.logService.error('Error message', new Error('Log example'), { key: 'value' });\n// ...and so on\n```\n\n### Transporters\n\nTwo transports are built in: **Console** and **Loki**.\n\n#### Console Transport\n\nEnabled by default. Controlled by:\n\n| Variable         | Required | Type   | Default                            |\n|------------------|:--------:|:------:|------------------------------------|\n| CONSOLE_SEVERITY | No       | string | `trace` if `NODE_ENV=local`; `warning` otherwise |\n\n#### Loki Transport\n\nPublishes logs to [Loki](https://grafana.com/oss/loki/) via its API. To enable, set `LOKI_URL`:\n\n| Variable      | Required | Type   | Default  |\n|---------------|:--------:|:------:|:--------:|\n| LOKI_URL      | Yes      | string |          |\n| LOKI_USERNAME | No       | string |          |\n| LOKI_PASSWORD | No       | string |          |\n| LOKI_SEVERITY | No       | string | `debug`  |\n\n---\n\n## Trace Module\n\nImplements distributed tracing using [OpenTelemetry](https://opentelemetry.io/docs/) with B3 header propagation. It automatically creates spans for inbound HTTP requests and outbound HTTP calls. You can also create custom spans using `startSpan()` from `TraceService`.\n\n### Usage\n\n```ts\nimport { TraceService } from '@bechara/crux';\n\n@Injectable()\nexport class FooService {\n  constructor(private readonly traceService: TraceService) {}\n\n  public readFoo(): Foo {\n    const span = this.traceService.startSpan('Reading Foo');\n    // ...\n    span.close();\n  }\n}\n```\n\n---\n\n## ORM Module\n\nAdds ORM support using [MikroORM](https://mikro-orm.io/), providing schema synchronization and a repository pattern.\n\n### Environment Variables\n\nAdd relevant connection variables (e.g., for MySQL or PostgreSQL) to your `.env`:\n\n```bash\nORM_TYPE='mysql'\nORM_HOST='localhost'\nORM_PORT=3306\nORM_USERNAME='root'\nORM_PASSWORD=''\nORM_DATABASE='test'\n\n# SSL options\nORM_SERVER_CA=''\nORM_CLIENT_CERTIFICATE=''\nORM_CLIENT_KEY=''\n```\n\n### Registration\n\n```ts\nimport {\n  AppEnvironment,\n  AppModule,\n  OrmConfig,\n  OrmModule,\n  PostgresSqlDriver,\n} from '@bechara/crux';\n\nvoid AppModule.boot({\n  configs: [OrmConfig],\n  imports: [\n    OrmModule.registerAsync({\n      inject: [OrmConfig],\n      useFactory: (ormConfig: OrmConfig) =\u003e ({\n        driver: PostgresSqlDriver,\n        host: ormConfig.ORM_HOST,\n        port: ormConfig.ORM_PORT,\n        dbName: ormConfig.ORM_DATABASE,\n        user: ormConfig.ORM_USERNAME,\n        password: ormConfig.ORM_PASSWORD,\n        pool: { min: 1, max: 25 },\n        sync: {\n          auto: true,\n          controller: true,\n          safe: ormConfig.NODE_ENV === AppEnvironment.PRODUCTION,\n        },\n        driverOptions: {\n          connection: {\n            ssl: {\n              ca: Buffer.from(ormConfig.ORM_SERVER_CA, 'base64'),\n              cert: Buffer.from(ormConfig.ORM_CLIENT_CERTIFICATE, 'base64'),\n              key: Buffer.from(ormConfig.ORM_CLIENT_KEY, 'base64'),\n            },\n          },\n        },\n      }),\n    }),\n  ],\n  providers: [OrmConfig],\n  exports: [OrmConfig, OrmModule],\n});\n```\n\nIf you prefer, you can replace `OrmConfig` with your own configuration class and secrets.\n\n### Creating an Entity\n\nRefer to the [MikroORM docs on defining entities](https://mikro-orm.io/docs/defining-entities) for detailed guidance.\n\n### Creating a Repository\n\nExtend the built-in abstract repository for additional ORM capabilities:\n\n```ts\nimport {\n  EntityManager,\n  EntityName,\n  OrmRepository,\n  Repository,\n} from '@bechara/crux';\nimport { User } from './user.entity';\n\n@Repository(User)\nexport class UserRepository extends OrmRepository\u003cUser\u003e {\n  constructor(\n    protected readonly entityManager: EntityManager,\n    protected readonly entityName: EntityName\u003cUser\u003e,\n  ) {\n    super(entityManager, entityName, {\n      defaultUniqueKey: ['name', 'surname'],\n    });\n  }\n}\n```\n\n### Creating a Controller\n\nCreate a controller that injects your repository to handle HTTP requests. For example, a CRUD controller:\n\n```ts\nimport {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  Patch,\n  Post,\n  Put,\n  Query,\n  OrmPageDto,\n} from '@bechara/crux';\nimport { UserRepository } from './user.repository';\nimport { UserCreateDto, UserReadDto, UserUpdateDto } from './user.dto';\nimport { User } from './user.entity';\n\n@Controller('user')\nexport class UserController {\n  constructor(private readonly userRepository: UserRepository) {}\n\n  @Get()\n  public async get(@Query() query: UserReadDto): Promise\u003cOrmPageDto\u003cUser\u003e\u003e {\n    return this.userRepository.readPaginatedBy(query);\n  }\n\n  @Get(':id')\n  public async getById(@Param('id') id: string): Promise\u003cUser\u003e {\n    return this.userRepository.readByIdOrFail(id);\n  }\n\n  @Post()\n  public async post(@Body() body: UserCreateDto): Promise\u003cUser\u003e {\n    return this.userRepository.createOne(body);\n  }\n\n  @Put()\n  public async put(@Body() body: UserCreateDto): Promise\u003cUser\u003e {\n    return this.userRepository.upsertOne(body);\n  }\n\n  @Put(':id')\n  public async putById(\n    @Param('id') id: string,\n    @Body() body: UserCreateDto,\n  ): Promise\u003cUser\u003e {\n    return this.userRepository.updateById(id, body);\n  }\n\n  @Patch(':id')\n  public async patchById(\n    @Param('id') id: string,\n    @Body() body: UserUpdateDto,\n  ): Promise\u003cUser\u003e {\n    return this.userRepository.updateById(id, body);\n  }\n\n  @Delete(':id')\n  public async deleteById(@Param('id') id: string): Promise\u003cUser\u003e {\n    return this.userRepository.deleteById(id);\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fetienne-bechara%2Fcrux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fetienne-bechara%2Fcrux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fetienne-bechara%2Fcrux/lists"}