matheusicaro-node-framework
v1.0.8
Published
My custom basic configuration setups for a quick build of services and APIs for Node.
Downloads
1,182
Maintainers
Readme
@mi-node-framework (matheusicaro)
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 😃👍
Installing
npm i matheusicaro-node-framework
Resources
Dependency Injection
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:
function registerProviders(this: DependencyRegistry): void {
this.container.register(ProviderTokens.MyProvider, {
useValue: new MyProvider()
});
}
export { registerProviders };
import { DependencyRegistry } from 'matheusicaro-node-framework';
let dependencyRegistry: DependencyRegistry;
const getDependencyRegistryInstance = (): DependencyRegistry => {
if (!dependencyRegistry) {
dependencyRegistry = new DependencyRegistry([ registerProviders, ...and others]);
}
return dependencyRegistry;
};
export { getDependencyRegistryInstance };
// 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);
//...
});
Logger
This is a custom logger already setup with winston. The logger will be printed in the files app console
How to use it:
import { DependencyInjectionTokens } from 'matheusicaro-node-framework';
class MyController {
constructor(
@inject(DependencyInjectionTokens.Logger)
private logger: LoggerPort
) {}
public handler(): Promise<void> {
this.logger.info('trace handler');
}
}
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;
Files location:
- 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"}}
Controller Base
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
RestControllerBase is the a base controller to be used in rest implementations, recommended express.
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 };
Testing
Factory
A factory to build objects easier with overrides for their props.
How to use it:
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 };
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
JestStub is a stub using
(jest.fn()
) for interface, types and objects.
You can easily stub/mock when you are testing.
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
VitestStub is a stub that uses Vitest (vi.fn()
) for interfaces, types, and objects.
You can easily stub/mock when testing with Vitest.
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
DeepStubObject is a deep typer to be used when JestStub or VitesStub don't return the deeper prop.
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);
});
Errors
You can use some custom errors in your business logic already implemented from ErrorBase
which handles with logger and traces.
ErrorBase
you can implement your own errors from this ErrorBase
.
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
InvalidArgumentError
is a type of error recommended to be used when an invalid argument is informed.
- 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' } });
userMessage
can be sent in the response automatically when using RestControllerBase (here)
InvalidRequestError
InvalidRequestError
is a type of error recommended to be used when an invalid argument is informed.
- 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' } });
userMessage
can be sent in the response automatically when using RestControllerBase (here)
InvalidStateError
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.
- 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' } });
userMessage
can be sent in the response automatically when using RestControllerBase (here)
NotFoundError
NotFoundError
is a type of error recommended to be used when a resource is not found.
- 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' } });
userMessage
can be sent in the response automatically when using RestControllerBase (here)