Lets say I have a clean architecture app in TypeScript, with a web implementation, and I have a User model. For every request to /user/:userId
I want to return this user, along some simple business logic: the length of characters in its name.
My question is if:
- The use case
UserGetOneUseCase
should call the UserRepo
directly to retrieve the user data; then instantiate the User model with this data, and finally use user
instance to run the business login stored in methods within this class —calculateNameCharacters
—. I have an example for this.
- The
User
model should be able to access the userRepo
, so the UserGetOneUseCase
would be able to instantiate it running some static method from it, and then run the business logic also stored in a method of the User model. I don't have an example for this one.
As a sidenote, the interfaces are omited here to avoid posting too much code.
OPTION 1: call UserRepo
from UserGetOneUseCase
:
I have a router, that receives the /users route. This is an implementation detail done in Express; but the important thing here is that I'm instantiating the UserRepo
, the userGetOneUseCase
—passing the instantiated userRepo
to it—, and the userGetOneController
, which receives the userGetOneUseCase
:
UsersRoute.get('/:userId', async (req: Request, res: Response, next: NextFunction) => {
const userRepo = new UserRepo();
const userGetOneUseCase = new UserGetOneUseCase(userRepo);
const userGetOneController = new UserGetOneController(userGetOneUseCase);
const response = await userGetOneController.execute(req, res, next);
return response;
});
Now, the controller: it receives the request, extracts the needed params, execute the useCase, receive the response from it, and if necessary transforms the response to return it to the route:
export class UserGetOneController extends BaseController {
useCase: IUserGetOneUseCase;
constructor(useCase: IUserGetOneUseCase) {
super();
this.useCase = useCase;
}
async executeImpl(req: Request, res: Response) {
const { userId } = req.params;
const userGetOneRequest: IUserGetOneRequest = {
userId
};
const response = await this.useCase.execute(userGetOneRequest);
return res.status(200).send(response);
}
}
Now we set the UserGetOneUseCase
:
export class UserGetOneUseCase implements IUserGetOneUseCase {
private userRepo: IUserRepo;
constructor(userRepo: IUserRepo) {
this.userRepo = userRepo;
}
public async execute(userGetOne) {
// Here we are calling the instance of `userRepo` to retrieve the user data. We don't have the instance of `User` yet.
const userData = await this.userRepo.userGetOne(userGetOne);
// Now, with the user data, we instantiate the `User` model.
const user = new User(userData);
// With the instance of the user model available, we can run some business logic stored as a method in its clas.
const charactersInName = user.calculateNameCharacters()
return {
...user,
charactersInName
};
}
}
And the userRepo
just returns the user data:
export class UserRepo implements IUserRepo {
public async userGetOne(userGetOne) {
return {
id: 1,
name: 'emile',
};
}
}
And finally, the User
model:
export class User {
id: number;
name: string;
constructor(user?) {
this.id = user?.id;
this.name = user?.name;
}
calculateNameCharacters(): number {
return this.name.length();
}
}
As already said, I don't have a working example for option 2.
Any idea will be welcome!