Skip to content

This framework is a pack of my custom basic configuration setups for a quick build of services and APIs for short projects like hackathons, studies, challenges etc.

License

Notifications You must be signed in to change notification settings

matheusicaro/matheusicaro-node-framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

18 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

@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 package

npm i matheusicaro-node-framework

Local Test

How to test the library locally?
1. in the library folder `mi-node-framework/`, run: ```terminal npm i npm run build npm link ```
  1. in the your project folder:
npm uninstall matheusicaro-node-framework
npm link 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:

1. Create your registers:
function registerProviders(this: DependencyRegistry): void {
  //
  this.register(RegistryScope.SINGLETON, ProviderTokens.MyProvider, new MyProvider());
}

export { registerProviders };
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);

  //...
});


Logger

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

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.

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 };


Testing

Factory

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

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

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

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);
  });


Errors

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' } });
  • userMessage can 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 & message
    • new 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 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 & message
    • new 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 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 & message
    • new 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)



About

This framework is a pack of my custom basic configuration setups for a quick build of services and APIs for short projects like hackathons, studies, challenges etc.

Resources

License

Stars

Watchers

Forks

Packages

No packages published