Clean Architecture with Nodejs, TypeScript and Mongo.
Published: 2021-10-21 16:47:45

In this case we will use the package [@tsclean/scaffold](https://www.npmjs.com/package/@tsclean/scaffold), which generates a structure based on Clean Architecture, it also has a CLI that helps us generating code to create the different components of the application. This package includes the `(Dependency Injection, DI)`, design pattern in object oriented programming (OOP) and the design principle `Inversion of Control, IoC)`, who resolves dependencies on application startup.

This time we are going to create a simple use case, but that contextualizes how the package works.

![Clean architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s7wyb2xzg060sigaiddk.jpeg)

##### _Use case for saving an entity in a database._

* We install the package globally on our pc.
```
npm i -g @tsclean/scaffold
```

* We create the project.
```
scaffold create:project --name=app
```
This command generates the following project structure and installs the dependencies to make the application work.
```
app
  |- node_modules
  |- src
     |- application
        |- config
            |- environment.ts
        |- app.ts
        |- singleton.ts
     |- deployment
        |- Dockerfile
     |- domain
        |- entities
        |- use-cases
           |- impl
     |- infrastructure
        |- driven-adapters
           |- adapters
           |- providers
        |- entry-points
           |- api
     |- index.ts
  |- tests
     |- domain
     |- infrastructure
  .env
  .env.example
  .gitignore
  package.json
  READMED.md
  tsconfig-build.json
  tsconfig.json
```

* We create the entity with the corresponding attributes, in this case we are going to store a user.

```
scaffold create:entity --name=user
```
This command will create the following structure in the domain layer.

```
src
  |- domain
     |- entities
        |- user.ts
```
```typescript
export type UserModel = {
  id: string | number;
  name: string;
  email: string;
}

export type AddUserParams = Omit<UserModel, 'id'>
```

* Now we create the interface that will communicate the domain layer with the infrastructure layer. This interface will contain the use case.

```
scaffold create:interface --name=add-user --path=models
```

This command will create the following structure in the domain layer.

```
src
  |- domain
     |- entities
        |- contracts
           |- add-user-repository.ts
```
```typescript
import {UserModel, AddUserParams} from "@/domain/models/user";

export const ADD_USER_REPOSITORY = "ADD_USER_REPOSITORY"

export interface IAddUserRepository {
    addUserRepository: (data: AddUserParams) => Promise<UserModel>;
}
```

* Now we create the service that is going to have all the logic to store the user.

```
scaffold create:service --name=add-user
```
This command will create the following structure in the domain layer.

```
src
  |- domain
     |- use-cases
        |- impl
           |- add-user-service-impl.ts
        | - add-user-service.ts
```

Interface to communicate the service with external layers.

```typescript
import {UserModel, AddUserParams} from "@/domain/models/user";

export const ADD_USER_SERVICE = "ADD_USER_SERVICE"

export interface IAddUserService {
  addUserService: (data: AddUserParams) => Promise<UserModel>
}
```

Service where we implement the interface.

```typescript
import {Service} from "@tsclean/core";
import {UserModel} from "@/domain/models/user";
import {AddUserParams} from "@/domain/models/user";
import {IAddUserService} from "@/domain/use-cases/add-user-service";
import {ADD_USER_REPOSITORY, IAddUserRepository} from "@/domain/models/gateways/add-user-repository";

@Service()
export class AddUserServiceImpl implements IAddUserService {
    constructor(
        @Adapter(ADD_USER_REPOSITORY) private readonly addUserRepository: IAddUserRepository
    ) {
    }

    async addUserService(data: AddUserParams): Promise<UserModel> {
        return await this.addUserRepository.addUserRepository(data);
    }
}
```

* Now we create the ORM adapter, in this case it is `mongoose`, but `sequelize` is also enabled.

```
scaffold create:adapter-orm --name=user --orm=mongo --manager=mongoose
```

This command creates the structure in the infrastructure layer, the `mongoose-instance-ts` file with the singleton to make the connection to `Mongo`, and updates the `singleton.ts` file adding the function that is then iterated in the `index.ts` to connect to the database.

```
src
  |- application
     |- config
        |- mongoose-instance.ts
     |- singleton.ts
  |- infrasctructure
     |- driven-adapters
        |- adapters
           |- orm
              |- mongoose
                 |- models
                    |- user.ts
                 |- user-mongoose-repository-adapter.ts
        |- providers
           |- index.ts
```

We create the model with the attributes of the entity that is in the domain, ensuring that the entries are the same.

```typescript
import { model, Schema } from "mongoose";
import { UserModel } from '@/domain/models/user';

const schema = new Schema<UserModel>({
    id:  { type: String },
    name: { type: String, required: true },
    email:  { type: String, required: true }
});

export const UserModelSchema = model<UserModel>('users', schema);
```

At this point the implementation of the interface in the adapter helps us with the communication between the layers, so they are decoupled, we apply the SOLID principle of Dependency Inversion Principle (DIP).

```typescript
import {UserModel, AddUserParams} from "@/domain/models/user";
import {IAddUserRepository} from "@/domain/models/gateways/add-user-repository";
import {UserModelSchema} from "@/infrastructure/driven-adapters/adapters/orm/mongoose/models/user";

export class UserMongooseRepositoryAdapter implements IAddUserRepository {
    
    async addUserRepository(data: AddUserParams): Promise<UserModel> {
        return await UserModelSchema.create(data);
    }
}
```

The scaffold creates the class with the __Singleton Pattern__ to handle the database connection.

```typescript
import { connect, set } from "mongoose";
import { Logger } from "@tsclean/core";
import { MONGODB_URI } from "@/application/config/environment";

/** Class that manages the configuration and connection to a MongoDB database */
export class MongoConfiguration {
    /** Private logger instance for logging purposes */
    private logger: Logger;

    /** Private static instance variable to implement the Singleton pattern */
    private static instance: MongoConfiguration;

    /** Private constructor to ensure that only one instance is created */
    private constructor() {
        /** Initialize the logger with the class name */
        this.logger = new Logger(MongoConfiguration.name);
    }

    /** Method to get the instance of the class, following the Singleton pattern */
    public static getInstance(): MongoConfiguration {
        if (!this.instance) {
            this.instance = new MongoConfiguration();
        }
        return this.instance;
    }

    /** Asynchronous method to manage the MongoDB database connection */
    public async managerConnectionMongo(): Promise<void> {
        /** Set MongoDB configuration option to enforce strict queries */
        set("strictQuery", true);

        try {
            /** Attempt to connect to the MongoDB database using the provided URI */
            await connect(MONGODB_URI);
            /** Log a success message if the connection is successful */
            this.logger.log(`Connection successfully to database of Mongo: ${MONGODB_URI}`);
        } catch (error) {
            /** Log an error message if the connection fails */
            this.logger.error("Failed to connect to MongoDB", error);
        }
    }
}
```

Then the `scaffold` adds an asynchronous function to the `singletonInitializers` array found in the `singleton.ts` file. This array is initialized in the `index.ts` file to create the connection instances.

```typescript
import { MongoConfiguration } from "@/application/config/mongoose-instance";

/**
   * This array has all the singleton instances of the application
   */
export const singletonInitializers: Array<() => Promise<void>> = [
    async () => {
        const mongooseConfig = MongoConfiguration.getInstance();
        await mongooseConfig.managerConnectionMongo();
    },
];
```

This class injects the dependencies, which are resolved at runtime.

```typescript
import {ADD_USER_REPOSITORY} from "@/domain/models/gateways/add-user-repository";
import {ADD_USER_SERVICE} from "@/domain/use-cases/add-user-service";

import {AddUserServiceImpl} from "@/domain/use-cases/impl/add-user-service-impl";
import {UserMongooseRepositoryAdapter} from "@/infrastructure/driven-adapters/adapters/orm/mongoose/user-mongoose-repository-adapter";

export const providers = [
    {
        useClass: UserMongooseRepositoryAdapter,
        provide: ADD_USER_REPOSITORY
    }
]

export const services = [
    {
        useClass: AddUserServiceImpl,
        provide: ADD_USER_SERVICE
    }
]
```

`index.ts`

```typescript
import "module-alias/register";

import helmet from 'helmet';
import { StartProjectInit } from "@tsclean/core";
        
import { AppContainer } from "@/application/app";
import { PORT } from "@/application/config/environment";
import { singletonInitializers } from "@/application/singleton";

async function init(): Promise<void> {
  /** Iterate the singleton functions */
  for (const initFn of singletonInitializers) {
    await initFn();
  }

  const app = await StartProjectInit.create(AppContainer)
  app.use(helmet());
  await app.listen(PORT, () => console.log(`Running on port: ${PORT}`))
}
   
void init().catch();
```

After this you must include in the `.env` file the mongo url to which you are going to connect.

At this point you can run the application on port `9000` and it should work.

* Now we create the controller, entry point to the application. If you name the controller as in the service, it creates code where you inject the dependency and create the corresponding route. When creating the service the name that we gave him was add-user, we must use that same name for the controller.

```
scaffold create:controller --name=add-user
```

This command will create the following structure in the infrasctructure layer.

```
src
  |- infrasctructure
     |- entry-points
        |- api
           |- add-user-controller.ts
           |- index.ts
```

```typescript
import {Mapping, Body, Post} from "@tsclean/core";
import {UserModel, AddUserParams} from "@/domain/models/user";
import {ADD_USER_SERVICE, IAddUserService} from "@/domain/use-cases/add-user-service";

@Mapping('api/v1/add-user')
export class AddUserController {

    constructor(
         @Adapter(ADD_USER_SERVICE) private readonly addUserService: IAddUserService
    ) {
    }

    @Post()
    async addUserController(@Body() data: AddUserParams): Promise<UserModel> {
        return await this.addUserService.addUserService(data);
    }
}
```

```typescript
import {AddUserController} from "@/infrastructure/entry-points/api/add-user-controller";

export const controllers = [
    AddUserController
]
```

All the components are ready for the use case to be ready, they just need to be put together.

In the app.ts file that is in the application layer we must make this configuration.

```typescript
import {Container} from "@tsclean/core";
import {controllers} from "@/infrastructure/entry-points/api";
import {adapters, services} from "@/infrastructure/driven-adapters/providers";

@Container({
    controllers: [...controllers],
    providers: [...services, ...adapters]
})

export class AppContainer {}
```

Congratulations, everything is ready, you can run the application in postman url http://localhost:9000/api/v1/add-user.

##### Final notes
 
This use case is basic, because it lacks some important elements, among them the validations:

* The email must be unique.
* Must have the correct format.
* Character length must be validated.

The developer can include the validator of his choice or do it with his own script. If you do it with an external library you must create an adapter for this purpose.