In this case we will use the 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.
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
|- domain
|- models
|- 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
|- models
|- user.ts
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
|- models
|- contracts
|- add-user-repository.ts
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.
import {UserModel, AddUserParams} from "@/domain/models/user";
export const ADD_USER_SERVICE = "ADD_USER_SERVICE"
export interface IAddUserService {
addUserService: (data: AddUserParams) => Promise<UserModel>
}
Servicio donde implementamos la interface.
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=mongoose
This command creates the following structure in the infrasctructure layer and updates the index.ts file with the configuration to connect to Mongo.
src
|- 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.
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).
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);
}
}
This class injects the dependencies, which are resolved at runtime.
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 updated for mongoose.
import 'module-alias/register'
import helmet from 'helmet';
import { connect } from 'mongoose';
import { StartProjectServer } from "@tsclean/core";
import { AppContainer } from "@/application/app";
import {MONGODB_URI, PORT} from "@/application/config/environment";
async function run(): Promise<void> {
await connect(MONGODB_URI);
console.log('DB Mongo connected')
const app = await StartProjectServer.create(AppContainer);
app.use(helmet());
await app.listen(PORT, () => console.log('Running on port: ' + PORT))
}
run();
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
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);
}
}
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.
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.
Top comments (4)
The correct syntax for the providers/index.ts files is like below. According to the example api from the developers at github.com/tsclean/api-example/blo...
export const adapters = [
{
useClass: UserMongooseRepositoryAdapter,
provide: ADD_USER_REPOSITORY,
},
];
export const services = [
{
useClass: AddUserServiceImpl,
provide: ADD_USER_SERVICE,
},
];
It is correct.
When trying to create the orm adapter, I get the error "🚫 You must first create the candidate entity to be imported to the ORM adapter.", I already created the userModel entity with the properties.
What can be?
How are you creating the adapter and for which ORM?
scaffold create:entity --name=user
scaffold create:adapter-orm --name=user
By convention you must use the same name of the entity.