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! ;-)