Search code examples
design-patternsarchitecturecrudsoftware-design

How to abstract over and unify different CRUD APIs


I am currently trying to build a library to abstract over the different CRUD Web APIs of our business units. I cant give an actual example, but here is the base problem:

We have N different APIs that all share more or less the same resources. There is for example a resource to create a user. For one API X, the resource is called "user", for another API Y its "users" and they all require different fields for a Create Request. X requires only a name and an address as string while Y wants name as a string but address as an object consisting of street, city and country.

My first intention was to use the Adapter pattern and define my own class User along with the API specific interfaces.

interface XUser {
   name: string;
   address: string;

   CREATE() : XUserWithId
}

interface YUser {
   name: string;
   address: {
      street: string;
      city: string;
      country: string
   };
   
   CREATE() : YUserWithId
}

interface IUser {
   name: string;
   street: string;
   city: string;
   country: string;
}

class User implements IUser {}

Then I wanted to write my Adapters to make my User class behave like the API specific objects:

class XUserAdapter implements XUser {
   constructor(user: IUser) {
      this.name = user.name;
      this.address = [user.street, user.city, user.country].join(", ");
   }
}


class YUserAdapter implements YUser {
   constructor(user: IUser){
      this.name = user.name;
      this.address = {
         street: user.street;
         city: user.city;
         country: user.country;
      }
   }
}

CREATEing a User Resource in APIs X and Y now boils down to creating one user object, putting it into the adapters and calling the adapters CREATE functions:

users: User[] = [];

tom = new User(name="Tom", street="Foostreet", city="Bartown", country="USA");

xuser = new XUserAdapter(tom);
yuser = new YUserAdapter(tom);

xuser = xuser.CREATE();
yuser = yuser.CREATE();

My Problem is that obviously calling CREATE will hand me back platform specific user objects that i want to be in the shape of my own User class. This would require two more adapters to "translate" to the opposite direction:

class UserFromYUser implements IUser {
   constructor(yUser: YUser) {
      ...
   }
}

Question: Am I missing the obvious? Is creating one adapter per platform specific resource really the way to go? I Read about the two-way adapter pattern in GoF but it was just a short hint. Do you guys have any other idea on how one could tackle this problem and how to split the code but maintain consistency across API interfaces?

Thank you in advance.


Solution

  • It looks like repository pattern is a way to go. As martinfowler.com says:

    Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.

    There is a very good article about what generic repository is. If your entities have the same methods, then try to use this pattern.

    This is a C# example of code:

    public interface IRepository<T>
    {
        void Insert(T entity);
        void Delete(T entity);
        void Update(T entity);
        IQueryable<T> GetAll();
        T GetById(int id);
    }