This framework is a pack of @matheusicaro custom basic configurations and setups for quickly building services and APIs in Node.js for short projects like hackathons, studies, challenges, etc. A bunch of resources here might be useful for our next project ππ
npm i matheusicaro-node-framework
How to test the library locally?
1. in the library folder `mi-node-framework/`, run: ```terminal npm i npm run build npm link ```
- in the your project folder:
npm uninstall matheusicaro-node-framework
npm link matheusicaro-node-framework
An abstraction class for an injection container for TypeScript using TSyring.
This abstraction allows me to use dependency injection in a contract defined in this framework without knowing the main provider (TSyring in v1.0.0).
If we decide to use another dependency injection provider, it will be easier for me to implement it here and update the following services with the new @mi-node-framework version.
How to use it:
1. Create your registers:
function registerProviders(this: DependencyRegistry): void {
//
this.register(RegistryScope.SINGLETON, ProviderTokens.MyProvider, new MyProvider());
}
export { registerProviders };- check the avaialbe scopes in RegistryScope.
2. Start your registry
import { DependencyRegistry } from 'matheusicaro-node-framework';
let dependencyRegistry: DependencyRegistry;
const getDependencyRegistryInstance = (): DependencyRegistry => {
if (!dependencyRegistry) {
dependencyRegistry = new DependencyRegistry([ registerProviders, ...and others]);
}
return dependencyRegistry;
};
export { getDependencyRegistryInstance };3. Injecting it
// application layer
import { inject } from 'matheusicaro-node-framework';
class MyController {
constructor(
@inject(ProviderTokens.MyProvider)
private myProvider: MyProviderPort
) {}
public handler(): Promise<void> {
this.myProvider.run();
}
}
export { MyController };// tests layer
describe('MyController', () => {
const provider = getDependencyRegistryInstance().resolve(ProviderTokens.MyProvider);
//...
});This is a custom logger already setup with winston. The logger will be printed in the files app console
How to use it:
1. by constructor injection
import { DependencyInjectionTokens } from 'matheusicaro-node-framework';
class MyController {
constructor(
@inject(DependencyInjectionTokens.Logger)
private logger: LoggerPort
) {}
public handler(): Promise<void> {
this.logger.info('trace handler');
}
}2. by resolving the instance
const logger = getDependencyRegistryInstance().resolve(ProviderTokens.MyProvider)
logger.info(message)
logger.info(message, { id: "...", status: "..." })
logger.error(message)
logger.error(message, { id: "...", status: "...", error })
logger.exception(error): void;Location for the log files
- file:
logs/exceptions.log
2024-11-27 14:47:58 [ ERROR ]==> uncaughtException: failed on starting the app Error: failed on starting the app
at Timeout._onTimeout (/Users/matheus.icaro/DEVELOPMENT/repositories/test/mi-gateway-service/src/app.ts:41:9)
at listOnTimeout (node:internal/timers:573:17)
at processTimers (node:internal/timers:514:7)
- file:
logs/combined.log
2024-11-27 14:50:53 [ ERROR ]==> {"message":"failed on starting the app","logData":{"trace_id":"fake_id","originalError":{"message":"its fail","stack":"Error: its fail\n at Timeout._onTimeout (/Users/matheus.icaro/DEVELOPMENT/repositories/test/mi-gateway-service/src/app.ts:44:11)\n at listOnTimeout (node:internal/timers:573:17)\n at processTimers (node:internal/timers:514:7)"}}}
2024-11-27 14:53:37 [ INFO ]==> {"message":"logging data for trace","logData":{"id":"fake_id"}}
The controller base is an abstract class with some useful resources to use, like handling errors and responding to the client with a pre-defined payload.
RestControllerBase is the a base controller to be used in rest implementations, recommended express.
How to use it?
import { RestControllerBase } from 'matheusicaro-node-framework';
class HealthController extends RestControllerBase {
constructor() {
super();
}
public async getHealth(_req: Request, res: Response): Promise<Response<HealthResponse>> {
try {
return res.status(200).json({ message: 'success' });
} catch (error) {
return this.handleErrorThenRespondFailedOnRequest({
error,
response: res,
responseData: {
status: 'FAILED',
time: new Date()
}
});
}
}
}
export { HealthController };A factory to build objects easier with overrides for their props.
How to use it:
1. create your factory
src/application/domain/entities/my-object.ts
// your entity/object
export interface MyObject {
id: string;
status: 'OPEN' | 'CLOSED' | 'IN_PROGRESS';
}tests/factories/my-object.factory.ts:
// declare any custom function to override specific params and make it easier to build specific objects
class MyObjectFactory extends Factory<MyObject> {
closed() {
return this.params({
status: 'CLOSED',
});
}
open() {
return this.params({
status: 'OPEN',
});
}
}
// return the default object when build it
const myObjectFactory = MyObjectFactory.define(() => ({
id: 'some-id',
status: 'IN_PROGRESS',
}));
export { myObjectFactory };2. Use it in your tests
import { myObjectFactory } from '../factories/my-object.factory.ts';
it('should find all closed status', async () => {
// build the object in the desired state pre-defined
const myObject = myObjectFactory.closed().build();
stubDatabase.findOne.mockResolvedValueOnce(myObject);
const result = await provider.findAllClosedStatus();
expect(result).toEqual([myObject]);
});
it('should find by id', async () => {
//override the build with any value for the fields from your object
const myObject = myObjectFactory.build({ id: "any id" });
stubDatabase.findOne.mockResolvedValueOnce(myObject);
const result = await provider.findById("any id");
expect(result).toEqual(myObject);
});JestStub is a stub using
(jest.fn()) for interface, types and objects.
You can easily stub/mock when you are testing.
How to use it?
import { jestStub } from 'matheusicaro-node-framework';
//...
const stubMyInterface = jestStub<MyInterface>();
const myClass = new MyClass(stubMyInterface)
//...
test('should stub function correctly and set id', async () => {
const userId = "id",
stubMyInterface.anyMethod.mockResolvedValueOnce(100);
const result = myClass.run(userId)
expect(result).toEqual(100);
expect(stubMyInterface).toHaveBeenCalledTimes(1);
expect(stubMyInterface).toHaveBeenCalledWith(userId);
});VitestStub is a stub that uses Vitest (vi.fn()) for interfaces, types, and objects.
You can easily stub/mock when testing with Vitest.
How to use it? π
import { vitestStub } from 'matheusicaro-node-framework';
//...
const stubMyInterface = vitestStub<MyInterface>();
const myClass = new MyClass(stubMyInterface)
//...
test('should stub function correctly and set id', async () => {
const userId = "id",
stubMyInterface.anyMethod.mockResolvedValueOnce(100);
const result = myClass.run(userId)
expect(result).toEqual(100);
expect(stubMyInterface).toHaveBeenCalledTimes(1);
expect(stubMyInterface).toHaveBeenCalledWith(userId);
});DeepStubObject is a deep typer to be used when JestStub or VitesStub don't return the deeper prop.
How to use it? π
import { jestStub, vitestStub, DeepStubObject } from 'matheusicaro-node-framework';
//...
let stubProvider: ProviderInterface & DeepStubObject<ProviderInterface>;
beforeAll(() => {
// with jestStub
stubProvider = jestStub<ProviderInterface>();
// with vitestStub
stubProvider = vitestStub<ProviderInterface>();
myClass = new MyClass(stubProvider);
});
test('should stub function correctly and set id', async () => {
//...
stubMyInterface.anyProp.deepProp.deeperProp.mockResolvedValueOnce(100);
});You can use some custom errors in your business logic already implemented from ErrorBase which handles with logger and traces.
you can implement your own errors from this ErrorBase.
How to use it?
class MyCustomErrorError extends ErrorBase {
constructor(message: string);
constructor(trace: InvalidStateErrorTrace);
constructor(message: string, trace?: InvalidStateErrorTrace);
constructor(messageOrTrace: string | InvalidStateErrorTrace, _trace?: InvalidStateErrorTrace) {
const { message, trace } = alignArgs(messageOrTrace, _trace);
super(ErrorCode.INVALID_STATE, InvalidStateError.name, message, {
userMessage: trace?.userMessage,
originalError: trace?.logData?.error,
...(trace?.logData && {
logs: {
data: trace?.logData,
level: LogLevel.ERROR,
instance: container.resolve<LoggerPort>(DependencyInjectionTokens.Logger)
}
})
});
}
}
export { InvalidStateError };InvalidArgumentError is a type of error recommended to be used when an invalid argument is informed.
This error will: π
- surface to the user with a known message for the invalid argument.
- Log automatically the error & "trace" field when it is present in the args
new InvalidArgumentError(message)=> do not error & message -new InvalidArgumentError(message, trace)=> do log message and trace fields
new InvalidArgumentError('invalid argument', { userMessage: 'friendly user message', logData: { traceId: 'id' } });userMessagecan be sent in the response automatically when using RestControllerBase (here)
InvalidRequestError is a type of error recommended to be used when an invalid argument is informed.
This error will: π
- surface to the user with a known message for the invalid request.
- Log automatically the error & "trace" field when it is present in the args
new InvalidRequestError(message)=> do not error & messagenew InvalidRequestError(message, trace)=> do log message and trace fields
new InvalidRequestError('invalid request', { userMessage: 'friendly user message', logData: { traceId: 'id' } });userMessagecan be sent in the response automatically when using RestControllerBase (here)
InvalidStateError is a type of error recommended to be used when an invalid state is found and your app is not able to handle with.
This error will: π
- surface to the user as a default error message (if not informed) once there is nothing the user can do at this point to fix the request
- Log automatically the error & "trace" field when it is present in the args
new InvalidStateError(message)=> do not error & messagenew InvalidStateError(message, trace)=> do log message and trace fields
new InvalidStateError('invalid state found', { userMessage: 'friendly user message', logData: { traceId: 'id' } });userMessagecan be sent in the response automatically when using RestControllerBase (here)
NotFoundError is a type of error recommended to be used when a resource is not found.
This error will: π
- surface to the user as an unknown error once there is nothing the user can do at this point to fix the request.
- Log automatically the error & "trace" field when it is present in the args
new InvalidStateError(message)=> do not error & messagenew InvalidStateError(message, trace)=> do log message and trace fields
new NotFoundError('doc was not found', { userMessage: 'friendly user message', logData: { docId: 'id' } });userMessagecan be sent in the response automatically when using RestControllerBase (here)