Clean Architecture Scaffold
This CLI creates the structure of a NodeJs and TypeScript project based on clean architecture to build REST full APIs, it comes with the initial configuration of an Express application as a NodeJs framework and this is located in the application layer
.
Implementation of the plugin
We install the plugin globally in our computer, to be able to access the commands that generate the tasks. the tasks.
npm i -g @tsclean/scaffold
Tasks
Project Generation
- We generate the project structure with the command
scaffold create:project
, which receives one parameter--name
.
scaffold create:project --name=[project name]
cd project name
Model Generation
-
The
scaffold create:entity
command will generate a model in thedomain layer [models]
, this task has--name
as parameter and this is required. The name must have a middle hyphen in case it is compound.Example:
--name=user
scaffold create:entity --name=user
Interface Generation
-
The
scaffold create:interface
command generates an interface, the location of the file is according to the component where it is required. where it is required. The name must have a hyphen in case it is a compound name.Example:
--name=user, --name=user-detail, --name=post-comments-user.
scaffold create:interface --name=user-detail --path=entities
scaffold create:interface --name=user-detail --path=service
scaffold create:interface --name=user-detail --path=infra
Interface Resource Generation
-
The command
scaffold create:interface-resource
generates an interface, this task has--name
and--resource
as parameters this is required. The name must be in lower case, as it is in the model.Example:
--name=user
scaffold create:interface-resource --name=user --resource
Service Generation
-
The
scaffold create:service
command will generate the interface and the service that implements it in thedomain layer [use-cases]
, this task has--name
as parameter and this is required. The name must be hyphenated if it is a compound name.Example:
--name=user, --name=user-detail, --name=post-comments-user.
scaffold create:service --name=user
Service Resource Generation
-
The
scaffold create:service-resource
command will generate the interface and the service that implements it in thedomain layer [use-cases]
, this task has--name
as parameter and--resource
this is required. The name must be in lower case, as it is in the model.Example:
--name=user --resource
scaffold create:service --name=user --resource
Adapter ORM Generation
-
The
scaffold create:adapter-orm
command will generate an adapter in theinfrastructure layer
, this task has--name
and--orm
as parameters this is required. The name of the--manager
parameter corresponds to the database manager. After the adapter is generated, the provider must be included in the app.ts file and then the name of the provider in the corresponding service must be passed through the constructor.Example:
--name=user --orm=sequelize --manager=mysql
-
By convention the plugin handles names in singular, this helps to create additional code that benefits each component. In this case when you create the adapter with the name that matches the entity in the domain models folder, it does the automatic import in all the component of the adapter.
- command to generate sequelize orm.
scaffold create:adapter-orm --name=user --orm=sequelize --manager=mysql
- command to generate the mongoose orm.
scaffold create:adapter-orm --name=user --orm=mongoose
Adapter Simple Generation
- The
scaffold create:adapter
command will generate an adapter simple in theinfrastructure layer
, this task has--name
as parameter and this is required.
scaffold create:adapter --name=jwt
Controller Generation
-
The
scaffold create:controller
command will generate a controller in theinfrastructure layer
, this task has--name
as parameter and this is required. The name must have a hyphen in case it is a compound name.Example:
--name=user, --name=user-detail, --name=post-comments-user.
scaffold create:controller --name=user-detail
Decorators
Decorators allow us to add annotations and metadata or change the behavior of classes, properties, methods, parameters and accessors.
@Services
Decorator to inject the logic of this class as a service.
@Services
export class UserServiceImpl {}
@Adapter
Decorator to keep the reference of an interface and to be able to apply the SOLID principle of Dependency Inversion.
// Constant to have the interface reference.
export const USER_REPOSITORY = 'USER_REPOSITORY';
export interface IUserRepository<T> {
save: (data: T) => Promise<T>
}
@service
export class UserServiceImpl {
constructor(
@Adapter(USER_REPOSITORY)
private readonly userRespository: IUserRepository
) {}
}
@Mapping
Decorator that allows us to create the path of an end point.
@Mapping('api/v1/users')
export class UserController {}
Decorators HTTP
@Get()
Decorator to solve a request for a specific resource.
@Post()
Decorator used to send an entity to a specific resource.
@Put()
Decorator that replaces the current representations of the target resource with the payload of the request.
@Delete()
Decorator that deletes the specific resource.
@Params()
Decorator to read the parameters specified in a method.
@Body()
Decorator that passes the payload of a method in the request.
@Mapping('api/v1/users')
export class UserController {
@Get()
getAllUsers() {}
@Get(':id')
getByIdUser(@Params() id: string | number) {}
@Post()
saveUser(@Body() data: T) {}
@Put(':id')
updateByIdUser(@Params() id: string | number, @Body data: T) {}
@Delete(':id')
deleteByIdUser(@Params() id: string | number) {}
}
Example of use case
- Create a user in the store
- Create the project.
scaffold create:project --name=store
- Create entity user
scaffold create:entity --name=user
// src/domain/entities/user.ts
export type UserEntity = {
id: string | number;
name: string;
email: string;
}
export type AddUserParams = Omit<UserEntity, 'id'>
- Create the contract to create the user.
scaffold create:interface --name=user --path=entities
// src/domain/models/contracts/user-repository.ts
import {AddUserParams, UserModel} from "@/domain/entities/user";
export const USER_REPOSITORY = 'USER_REPOSITORY';
export interface IUserRepository {
save: (data:AddUserParams) => Promise<UserModel>;
}
- Create services user
scaffold create:service --name=user
// src/domain/use-cases/user-service.ts
import {AddUserParams, UserEntity} from "@/domain/entities/user";
export const USER_SERVICE = 'USER_SERVICE';
export interface IUserService {
save: (data: AddUserParams) => Promise<UserEntity>;
}
// src/domain/use-cases/impl/user-service-impl.ts
import {Adapter, Service} from "@tsclean/core";
import {IUserService} from "@/domain/use-cases/user-service";
import {UserModel} from "@/domain/models/user";
import {IUserRepository, USER_REPOSITORY} from "@/domain/models/contracts/user-repository";
@Service()
export class UserServiceImpl implements IUserService {
constructor(
// Decorator to keep the reference of the Interface, by means of the constant.
@Adapter(USER_REPOSITORY)
private readonly userRepository: IUserRepository,
) {}
/**
* Method to send the data to the repository.
* @param data {@code UserEntity}
*/
async save(data: AddUserParams): Promise<UserEntity> {
// Send the data to the repository.
return this.userRepository.save({...data});
}
}
- Create mongoose adapter and additionally you must include the url of the connection in the
.env
file
scaffold create:adapter-orm --name=user --orm=mongoose
// src/infrastructure/driven-adapters/adapters/orm/mongoose/models/user.ts
import { UserEntity } from '@/domain/entities/user';
import { model, Schema } from "mongoose";
const schema = new Schema<UserEntity>({
id: {
type: String
},
name: {
type: String
},
email: {
type: String
}
}, {strict: false});
export const UserModelSchema = model<UserEntity>('users', schema);
// src/infrastructure/driven-adapters/adapters/orm/mongoose/user-mongoose-repository-adapter.ts
import {AddUserParams, UserEntity} from "@/domain/entities/user";
import {IUserRepository} from "@/domain/models/contracts/user-repository";
import {UserModelSchema as Schema} from "@/infrastructure/driven-adapters/adapters/orm/mongoose/models/user";
export class UserMongooseRepositoryAdapter implements IUserRepository {
async save(data: AddUserParams): Promise<UserEntity> {
return Schema.create(data);
}
}
- Pass the key:value to do the dependency injections
// src/infrastructure/driven-adapters/providers/index.ts
import {USER_SERVICE} from "@/domain/use-cases/user-service";
import {USER_REPOSITORY} from "@/domain/models/contracts/user-repository";
import {UserServiceImpl} from "@/domain/use-cases/impl/user-service-impl";
import {
UserMongooseRepositoryAdapter
} from "@/infrastructure/driven-adapters/adapters/orm/mongoose/user-mongoose-repository-adapter";
export const adapters = [
{
// Constant referring to the interface
provide: USER_REPOSITORY,
// Class that implements the interface
useClass: UserMongooseRepositoryAdapter
}
];
export const services = [
{
// Constant referring to the interface
provide: USER_SERVICE,
// Class that implements the interface
useClass: UserServiceImpl
}
];
- Create controller user
scaffold create:controller --name=user
// src/infrastructure/entry-points/api/user-controller.ts
import {Mapping, Post, Body} from "@tsclean/core";
import {AddUserParams, ModelUser} from "@/domain/models/user";
import {IUserService, USER_SERVICE} from "@/domain/use-cases/user-service";
@Mapping('api/v1/users')
export class UserController {
constructor(
// Decorator to keep the reference of the Interface, by means of the constant.
@Adapter(USER_SERVICE)
private readonly userService: IUserService
) {}
@Post()
async saveUserController(@Body() data: AddUserParams): Promise<ModelUser | any> {
// Send the data to the service through the interface.
const user = await this.userService.save(data);
return {
message: 'User created successfully',
user
}
}
}
- Finally you can test this endpoint
http://localhost:9000/api/v1/users
, methodPOST
in the rest client of your choice and send the corresponding data.