Crux
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).
Install / Use
/learn @etienne-bechara/CruxREADME
CRUX
CRUX 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.
- Framework: NestJS
- HTTP Server: Fastify
- HTTP Client: Fetch
- Caching: ioredis (distributed) or in-memory (local)
- ORM: MikroORM
- OpenAPI: Scalar
- Logs: Loki
- Metrics: Prometheus
- Tracing: Tempo with OpenTelemetry
Disclaimer
This 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.
Installation
-
Create and initialize a new Node.js project, then install TypeScript, its types, a live-reload tool, and this package.
We recommend using pnpm as your package manager, and ts-node-dev for live reloading:
mkdir my-project cd my-project git init npm init -y npm i -g pnpm pnpm i -D typescript @types/node ts-node-dev pnpm i -E @bechara/crux tsc --init -
Create a
main.tsfile in a/sourcefolder with the following content:// /source/main.ts import { AppModule } from '@bechara/crux'; void AppModule.boot(); -
Add a
devscript in yourpackage.json:{ "scripts": { "dev": "tsnd --exit-child --rs --watch *.env --inspect=0.0.0.0:9229 ./source/main.ts" } } -
Start the application:
pnpm devYou can test it by sending a request to
GET /. You should receive a successful response with a204status code.
Development
Using this framework mostly follows the official NestJS Documentation. Familiarize yourself with the following core NestJS concepts before continuing:
Key Differences
-
Imports from
@bechara/crux
All NestJS imports, such as@nestjs/commonor@nestjs/core, are re-exported by@bechara/crux.
Instead of:import { Injectable } from '@nestjs/common';use:
import { Injectable } from '@bechara/crux'; -
Automatic Module Loading
Any file ending with*.module.tsin your source folder is automatically loaded bymain.ts. You don’t need to create a global module importing them manually.
Instead of:@Global() @Module({ imports: [ FooModule, BarModule, BazModule, ], }) export class AppModule { }simply do:
import { AppModule } from '@bechara/crux'; void AppModule.boot(); // FooModule, BarModule, and BazModule are automatically loaded // as long as they're in the source folder and named *.module.ts
Testing
Testing 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.
Usage
In your *.service.spec.ts, add a beforeAll() hook to compile an application instance:
import { AppModule } from '@bechara/crux';
describe('FooService', () => {
let fooService: FooService;
beforeAll(async () => {
const app = await AppModule.compile();
fooService = app.get(FooService);
});
describe('readById', () => {
it('should read a foo entity', async () => {
const foo = await fooService.readById(1);
expect(foo).toEqual({ name: 'bob' });
});
});
});
If you need custom options, the compile() method supports the same boot options as boot().
Run all tests with:
pnpm test
Or a specific set:
pnpm test -- foo
Curated Modules
Below are details about the main modules in this framework and how to use them.
Application Module
Acts as the entry point, wrapping other modules in this package and automatically loading any *.module.ts in your source folder.
By default, it serves an HTTP adapter using Fastify. The following custom enhancers are globally applied:
- app.interceptor.ts: A timeout interceptor that cancels requests exceeding the configured runtime.
- app.filter.ts: An exception filter integrated with the logging service for standardized error output.
- ClassSerializer for response serialization.
- ValidationPipe for DTO validation and transformation.
Environment Configuration
| Variable | Mandatory | Type | |-----------|:--------:|--------------------------------------------------------| | NODE_ENV | Yes | AppEnvironment |
Module Options
When booting your application, you can configure options as described in AppBootOptions:
import { AppModule } from '@bechara/crux';
void AppModule.boot({
// See AppBootOptions for detailed properties
});
Provided options will be merged with the default configuration.
Configuration Module
Allows asynchronous population of secrets through *.config.ts files containing configuration classes.
Decorate 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.
Usage
- Create a
*.config.tsfile with a class decorated by@Config(). - Decorate any properties with
@InjectSecret(). - Optionally, apply
class-validatorandclass-transformerdecorators for validation and transformation.
Example:
import { Config, InjectSecret, IsUrl, IsString, Length, ToNumber } from '@bechara/crux';
@Config()
export class FooConfig {
@InjectSecret()
@IsUrl()
FOO_API_URL: string;
@InjectSecret({ key: 'foo_authorization' })
@IsString() @Length(36)
FOO_API_KEY: string;
@InjectSecret({ fallback: '15' })
@ToNumber()
FOO_API_MAX_CONCURRENCY: number;
}
Use the configuration in your module and services:
@Injectable()
export class FooService {
constructor(private readonly fooConfig: FooConfig) {}
public async readFooById(id: number) {
console.log(this.fooConfig.FOO_API_MAX_CONCURRENCY);
// ...
}
}
Context Module
Provides 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.
Usage
import { ContextService } from '@bechara/crux';
@Injectable()
export class FooService {
constructor(private readonly contextService: ContextService) {}
public getRequestAuthorization() {
const req = this.contextService.getRequest();
return req.headers.authorization;
}
public getUserId() {
return this.contextService.getMetadata('userId');
}
public setUserId(userId: string) {
this.contextService.setMetadata('userId', userId);
}
}
Documentation Module
Generates OpenAPI documentation using NestJS OpenAPI Decorators.
- User interface: available at
/docs - OpenAPI spec: available at
/docs/json
Http Module
Provides a wrapper over Node.js Fetch API, exposing methods to make HTTP requests. Its scope is transient: every injection yields a fresh instance.
Basic Usage
In your module:
import { HttpModule } from '@bechara/crux';
@Module({
imports: [HttpModule.register()],
controllers: [FooController],
providers: [FooService],
})
export class FooModule {}
In your service:
import { HttpService } from '@bechara/crux';
@Injectable()
export class FooService {
constructor(private readonly httpService: HttpService) {}
public async readFooById(id: number) {
return this.httpService.get('https://foo.com/foo/:id', {
replacements: { id },
});
}
}
Async Registration
To configure base parameters (host, headers, API keys, etc.) using environment secrets:
import { HttpAsyncModuleOptions, HttpModule } from '@bechara/crux';
const httpModuleOptions: HttpAsyncModuleOptions = {
inject: [FooConfi
