Search code examples
typescriptbackendnestjstypeormclean-architecture

How to correct implement the Repository Adapter?


I started learning how to build Clean Architecture based on TypeScript and NestJS. Everything was ok until I started implementing the Repository Adapter and Controllers. The main problem is incompatible return types for the API methods and use-cases.

The idea was to put entities and use-cases in the core folderin which use-cases use repository adapter (through DI). This adapter implements the repository interface from the core folder too.

Implementation of the repository adapter contains in app. app also contains NestJS implementation, TypeOrm entities etc. But also I want to use a repository for some controllers like getAll query.

!!!AND THE PROBLEM!!!

For almost every command, I have to use Mappers because TypeORM entity and Domain Entity are incompatible types. And I think that's okay if we pass that data to the use-case because we need to transform data from TypeOrm shape to the domain shape.

But if I just call the repository adapter method in the controller I need to map data back again... And I don't know how to skip unnecessary steps. In my imagination, I can just call the repository method in app service, and that is it. And if I skip the mapping back then all data properties will have the prefix _(

Maybe someone met the same problem?

//CORE ZONE

Account entity (/domain/account):

export type AccountId = string;
export class Account {
constructor(
  private readonly _id: AccountId,
  private readonly _firstName: string
) {}

  get id(): AccountId {
   return this._id;
  }

  get firstName() {
   return this._firstName;
  }
}

Repository interface (repositories/account-repository):

import { Account } from '../domains/account';

export interface AccountRepository {
  getAccountById(id: string): Promise<Account>;
  getAllAccounts(): Promise<Account[]>;
}

Example of use-case with repositoty:

import { AccountRepository } from '../../repositories/account-repository';

export class ToDoSomething {
  constructor(private readonly _accountRepository: AccountRepository) {}

  async doSomethingWithAccount(command): Promise<boolean> {
    const account = await this._accountRepository.getAccountById(
      command.accountId,
    );

    if (!account) {
      return false;
    }

    return true;
  }
}

//APP ZONE

Repository Adapter:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Account } from '../../../../core/domains/account';
import { AccountRepository } from '../../../../core/repositories/account-repository';
import { AccountEntity } from '../account.entity';
import { AccountMapper } from '../account.mapper';

@Injectable()
export class AccountRepositoryAdapter implements AccountRepository {
  constructor(
    @InjectRepository(AccountEntity)
    private readonly _accountRepository: Repository<AccountEntity>,
  ) {}

  async getAccountById(id: string): Promise<Account> {
    return this._accountRepository.findOne({ id: id });
    // will return { id: 1, firstName: "name" }
    // and because I need to use MapToDomain
  }

  async getAllAccounts(): Promise<Account[]> {
    return this._accountRepository.find();
    // Here too I need to use MapToDomain for every item
  }
}

TypeOrm Account:

import {
  Column,
  Entity,
  PrimaryGeneratedColumn,
} from 'typeorm';

@Entity({ name: 'account' })
export class AccountEntity {
  @PrimaryGeneratedColumn()
  id: string;

  @Column()
  firstName: string;
}

Solution

  • In Clean Architecture the control and data flow is usually like this: controller takes request from view (e.g. web app) and converts it into a request model which is than passed to the use case. The use case reads from the request model what it should compute and uses repository to interact with domain entities to finally create a response model. The response model is then passed to presenter (which might be same class as controller) which converts this into a response for the view.

    The controller usually does not interact with domain entities or even ORM types.

    Check out my blog series on implementing Clean Architecture for more details: http://www.plainionist.net/Implementing-Clean-Architecture-Controller-Presenter/