0

Consider the following trivial example in light of clean architecture, how can I improve on it? Specifically, I'm concerned with the app.ts file floating as it is. It acts as the composition root, where all dependencies are wired up but it just... doesn't look right.

// domain/repository.interface.ts
import { User } from './user.interface'
import { UserData } from './user-data.interface'

export interface UserRepository {
  saveUser(userData: UserData): Promise<User>
}
// domain/usecase.interface.ts
import { User } from './user.interface'
import { UserData } from './user-data.interface'

export interface CreateUserUseCase {
  execute(userData: UserData): Promise<User>
}
// domain/create-user.usecase.ts
import { CreateUserUseCase } from './usecase.interface'
import { UserRepository } from './repository.interface'
import { User } from '../user.interface'
import { UserData } from './user-data.interface'

class CreateUserUseCaseImpl implements CreateUserUseCase {
  constructor(private userRepository: UserRepository) {}

  async execute(userData: UserData): Promise<User> {
    // Perform validation logic on the userData
    // ...
    // Use the UserRepository to save the user data
    const savedUser = await this.userRepository.saveUser(userData)
    return savedUser
  }
}

export default CreateUserUseCaseImpl
// domain/user.interface.ts
export interface UserData {
  // Define the properties for user data
}
// domain/user-data.interface.ts
export interface User {
  // Define the properties for the User entity
}
// infra/persistence/mongodb-user.repository.ts
import { UserRepository } from '../domain/repository.interface'
import { User } from '../domain/user.interface'
import { UserData } from '../domain/user-data.interface'

class MongoDBUserRepository implements UserRepository {
  async saveUser(userData: UserData): Promise<User> {
    // Implementation to save the user to the MongoDB database
    // ...
    // Return the saved user
    return savedUser
  }
}

export default MongoDBUserRepository
// infra/http/main.ts
import { userController } from '../../app.ts'

// Assuming you have some Express server set up
// Example API route for user creation
app.post('/users', async (req, res) => {
  const userData: UserData = req.body

  try {
    const createdUser = await userController.createUser(userData)
    res.json(createdUser)
  } catch (error) {
    // Handle errors
    res.status(500).json({ error: 'Failed to create user.' })
  }
})
// app.ts
import { UserRepository } from './domain/repository.interface'
import { CreateUserUseCase } from './domain/usecase.interface'
import MongoDBUserRepository from './infra/mongodb.user.repository'
import CreateUserUseCaseImpl from './domain/create.user.usecase'

// Dependency Injection and Composition Root
const userRepository: UserRepository = new MongoDBUserRepository()
const createUserUseCase: CreateUserUseCase = new CreateUserUseCaseImpl(userRepository)
const userController: UserController = new UserController(createUserUseCase)

export { userController }

Other approach would be to eschew app.ts in favour of another layer, say controllers, and import that in infra/http/main.ts:

// controllers/user-controller.ts
import { UserRepository } from '../domain/repository.interface'
import { CreateUserUseCase } from '../domain/usecase.interface'
import MongoDBUserRepository from '../infra/mongodb.user.repository'
import CreateUserUseCaseImpl from '../domain/create.user.usecase'

// Dependency Injection and Composition Root
const userRepository: UserRepository = new MongoDBUserRepository()
const createUserUseCase: CreateUserUseCase = new CreateUserUseCaseImpl(userRepository)
const userController: UserController = new UserController(createUserUseCase)

export { userController }

Yet the above clearly violates the dependency inversion principle as it depends on infra layer. Perhaps I'm all wrong here, and there's a better approach?

Penny for your thoughts! ;-)

boojum
  • 669
  • 2
  • 9
  • 31

2 Answers2

1

It acts as the composition root, where all dependencies are wired up but it just... doesn't look right.

IMO composition root or manual DI is fine in most cases.

Here is a shot read and a long read:

https://itijs.org/docs/why#when-should-i-use-a-dependency-injection-framework

https://blog.ploeh.dk/2012/11/06/WhentouseaDIContainer/

Disclaimer: Typescript DI library (iti) author here

1

I'm concerned with the app.ts file floating as it is. It acts as the composition root, where all dependencies are wired up but it just... doesn't look right.

Yes it looks right. Finally you need one file whose responsibility is to instantiate and wire the objects. I can't say why you have that feeling that it dosen't look right. I usually do it the same way and feel fine with it.

The only thing I do different is to write one file for each use case wiring. So I would have a create-user-use-case-config.ts. I name it *-config.ts, because I have a Java Spring background. Maybe factory or any other name is appropriate for you.

Other approach would be to eschew app.ts in favour of another layer, say controllers, and import that in infra/http/main.ts: ... Yet the above clearly violates the dependency inversion principle as it depends on infra layer. Perhaps I'm all wrong here, and there's a better approach?

You gave yourself the answer. We often tend to reduce the number of files we write, because we think we get lost if the file count increases. But my experience is that the opposite is true. If we separate each responsibility in an own file we gain clarity. If the number of files increases we should use folders to structure our application.

Sometimes we can reduce the amount of files, if we keep things with a high cohesion together. E.g. if you would have dedicated request and response models for a use case, you could place that models and the use case interface in one file. If you use a dedicated repository interface for each use case implementation, you can place the use case implementation and the repository interface in one file. These dedicated interfaces and objects are an application of the interface segregation principle.

René Link
  • 48,224
  • 13
  • 108
  • 140