{"id":28381040,"url":"https://github.com/bogdan-lyashenko/data-layer-for-ui","last_synced_at":"2025-06-25T01:31:57.714Z","repository":{"id":75624249,"uuid":"90540148","full_name":"Bogdan-Lyashenko/Data-layer-for-UI","owner":"Bogdan-Lyashenko","description":"A proper way to work with external data API","archived":false,"fork":false,"pushed_at":"2017-05-11T08:25:56.000Z","size":13,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-30T03:40:03.509Z","etag":null,"topics":["architectural-patterns","clean-code","design-patterns","javascript","rest-api","reusable"],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Bogdan-Lyashenko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-05-07T14:52:57.000Z","updated_at":"2025-04-19T06:34:37.000Z","dependencies_parsed_at":"2023-06-07T03:45:27.694Z","dependency_job_id":null,"html_url":"https://github.com/Bogdan-Lyashenko/Data-layer-for-UI","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Bogdan-Lyashenko/Data-layer-for-UI","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bogdan-Lyashenko%2FData-layer-for-UI","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bogdan-Lyashenko%2FData-layer-for-UI/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bogdan-Lyashenko%2FData-layer-for-UI/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bogdan-Lyashenko%2FData-layer-for-UI/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Bogdan-Lyashenko","download_url":"https://codeload.github.com/Bogdan-Lyashenko/Data-layer-for-UI/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bogdan-Lyashenko%2FData-layer-for-UI/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261785242,"owners_count":23209268,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architectural-patterns","clean-code","design-patterns","javascript","rest-api","reusable"],"created_at":"2025-05-30T03:37:47.576Z","updated_at":"2025-06-25T01:31:57.702Z","avatar_url":"https://github.com/Bogdan-Lyashenko.png","language":null,"readme":"# Data layer for UI\n\n### The problem \n\nHow do we work with API to get some data from back-end? In the worst case scenario it looks just like direct call of *fetch* or *$.ajax* from a view layer.  \n\nFor example, we fetch some user details to display it on our 'Home' page:\n```javascript\n//home/users.js\nfetch(‘api/users/get?id=’ + userId)\n```\nand, the same one API call in several more places in the code and so on.\n```javascript\n//details/form.js\nfetch(‘api/users/get?id=’ + userId)\n```\nLooks like un-controlled code duplication, right?\nBut usually, it even has a bit more lines of code, additional parameters and response data preparing, etc. So it comes like that instead.\n```javascript\nconst url = ‘api/users/get?id=’ + 'username.lastname.id';\n\nif (specialUsers.contains(userId)) {\n    url += '?identity-prove=keycode';\n}\n\nfetch(url, {\n    method: 'post',\n    headers: {\n        Authority: 'tokenAjuib34=='\n    }\n}).then((data)=\u003e {\n    if (!data) {\n        return Promise.reject('Got empty profile data');\n    }\n    \n    return data.friends.map((friend)=\u003e {\n        return {\n            ...friend,\n            importance: GLOBAL_IMPORTANCE_CODE\n        };\n    });\n});\n```\nSo, you got the idea. And now, multiply this code in 2x times (because we have it in two files), and imagine for 5x more API calls, and several more different API providers. Sounds crazy. But we can fix it. First and (obviously) not right way to do that is make small refactoring and move all API call related code to separate method and maybe to separate file.  \n\n```javascript\n//api/user.js\nconst USER_URL = ‘api/users/get?id=’;\n\nexport const getUserById = (id) =\u003e {\n  const url = USER_URL; \n\t\t\n  if (specialUsers.contains(userId)) {\n      url += '?identity-prove=keycode';\n  }\n\n  fetch(url, {\n      method: 'post',\n      headers: {\n          Authority: 'tokenAjuib34=='\n      }\n  }).then((data)=\u003e {\n      if (!data) {\n          return Promise.reject('Got empty profile data');\n      }\n\n      return data.friends.map((friend)=\u003e {\n          return {\n              ...friend,\n              importance: GLOBAL_IMPORTANCE_CODE\n          };\n      });\n  });\n}\n```\nand then we can use it from our controllers etc. Like\n```javascript\n//details/form.js\nimport {getUserById} from 'api/services/user';\n...\ngetUserById('username1.lastname1.id').then((data)=\u003e {\t\n    //use user data for UI form\n});\n```\nand from another place\n```javascript\n//home/users.js\nimport {getUserById} from 'api/services/user';\n...\ngetUserById('username2.lastname2.id').then((data)=\u003e {\t\n    //display user on home page\n});\n``` \nAnd everything seems to be working fine, we can reduce code duplication so much, we even have USER_URL as constant in one place, so if url is changed on server side, we change it here as well, everything is under control, right?.   No, that's just an illusion. \n\nYou will be surprised how soon you will need to do change like \n```javascript\n//api/user.js\n...\nexport const getUserById = (id, homePageOnlyFlag) =\u003e {\n  const url = USER_URL; \n  \n  if (homePageOnlyFlag) {\n      url += '?target=from-home';\n  }\n\n  fetch(url, {\n      method: 'post',\n      headers: {\n...\n``` \nto extend or change behaviour for calls from 'Home' page only. You can mess it up with more flags, sometimes it will be easy when it's just extension of url, sometimes it will be harder, when you need to remove some already existing behaviour. And some moment it will be so messy that you will just copy half of the logic to new method 'getUserByIdCallFromHomePage' to avoid that flags like 'homePageOnlyFlag' and will use separate method for the same API call from different part of your application. That's exactly not that thing which we were expecting to have when started refactoring, right? That actually happened because  the way how we refactored it was wrong from the beginning. \n\nThere is a key thing in work with server API calls, it's hard to standardize configuration and parameters for different calls of one API end-point. Well, it's possible to put some common behaviour as 'basic' (or 'default') directly to the call configuration, but it's on our own risk, so more 'basic' configuration we have, so higher risk to face the case where some logic from default behaviour is conflicting with currently needed case. And vise-versa, so less 'default' behaviour we define, so more duplicated code we put into parents code (files), which trigger API call. We, kind of need 'golden middle way' for that. \n\n### The solution \nThe solution is based on the way how we plan to handle required mutations, that behaviour changes which are needed for some corner cases, and, some additional abstractions, to make the code flexible. \nThe key points are:\n* A parent should have ability to change API call behaviour externally, instead of only saying to API service what is needed right now. It will help to avoid logic for all cases inside 'getUserById' method. \n* Postponed API call. A parent should trigger call when it's known that call is configured properly for exact case.\n* API call method (like 'getUserById') should contain 'current call parameters' state. This will make possible to change configuration at any moments of time.\n* API call method should provide interfaces to modify parameters state or add pre/post interceptors for call.\n* An abstractions layer of models and services should be added\n\nAlright, let's check the code can looks like\n```javascript\n//home/users.js\n...\nimport user from 'api/models/user';\n...\nuser.getDetails('username2.lastname2.id').perfrom().then((data)=\u003e {\t\n    //display user on home page\n});\n```\nThe first thing you can notice is path to 'user', it's 'api/models/user'.\nThe directories sctructure could be as following\n```javascript\nproject/\n-/api/\n--/models\n---/user\n---/post\n---/...\n--/services\n---/friends\n```\nThe idea is to keep models as 'small building blocks', and more complex logic, which involves several models or several API calls to get final data put to services. \nSo, method in a model can be like:\n```javascript\n//api/models/user.js\n...\nconst getDetails = (userId) =\u003e {\t\n    //one API call\n});\n```\nand let's check method in a service. For example, we can imagine the next scenario: we want to know all friends who liked our last post. According to API implementation we need to combine two requests, get last user post and then get post 'likes'.\n```javascript\n//api/services/friends.js\nimport user from 'api/models/user';\nimport post from 'api/models/post';\n...\nconst getFriendLikes = (userId) =\u003e {\n    //1) user.getLastPost\n    //2) post.getFriendsLikeForPost\n});\n```\n\nThere is an important moment, despite  on the fact services includes  models, 'services layer' in not above 'models level', they both are on the same abstraction level, you can use both of them directly from 'view layer' (controllers, etc.). That's important because if put models below services, half of services interfaces will be idle wrappers of models methods, like\n```javascript\n//api/services/friends.js\nimport user from 'api/models/user';\nimport post from 'api/models/post';\nimport like from 'api/models/like';\n...\nconst getUser = () =\u003e {\t\n    user.getUser()   \n});\n\nconst getPost = () =\u003e {\t\n    post.getPost()   \n});\n\nconst getLike = () =\u003e {\t\n    like.getLike()   \n});\n```\nit looks and actually is meaningless, so, just avoid this.\n\nAlright, with a structure it is clear. Let's see initial problem with basic API parameters.\n```javascript\n//home/users.js\n...\nuser.getDetails('username2.lastname2.id')\n    .perfrom()\n    .then((data)=\u003e \n        //display user on home page\n    });\n```\nYour remember the point we've mentioned about postponed call. You can see 'user.getDetails' returns something else now, not a promise as we used to see. *That's a key trick.*.\nImagine that we allowed to do something like that\n```javascript\n//home/users.js\n...\nuser.getDetails('userId')\n  .addHeaders({\n      ['Header-Only-For-Home-Page']: 'additionalToken=='\n  })\n  .removeUrlParams(['listOffest'])\n  .addInterceptor({\n  \tpostCall: () =\u003e {\n    \tlogger.info('Heave API call performed from home page.')\n    }\n  })\n  .perfrom()\n  .then((data)=\u003e {\t\n      //display user on home page\n  });\n```  \n\nLooks pretty powerful, right. Remember, we still have some default behavior defined in the model, but exactly for one API call case we can change it in any ways we want without affecting other call, without modifying model implementation.\n\nAlright, let's check what's inside 'user.getDetails' method. \n\n```javascript\n//api/models/user.js\n...\ngetDetails = (userId) =\u003e {\t\n    const apiConfigCall = new ApiCallConfigurationObject();\n    \n    apiConfigCall.setBaseUrl(URL_CONST.USER_DETAILS);\n    apiConfigCall.addUrlParams(userId);\n    \n    return apiConfigCall;\n}  \n```\n\nObviously, it's some special object which contains methods to change request state parameters. Let's see below the pseudo-code how it can be implemented\n\n```javascript\n//ApiCallConfigurationObject\nclass ApiCallConfigurationObject() {\n  const state = {\n      urlParams: {},\n      headers: {},\n      interceptors: {},\n      ...\n  }\n\n  addHeaders(config) {\n      //for each header in config\n      //put it into state.headers map\n\n     return this;\n  }\n    \n  perform() {  \t\n    //gather all config from this.state and prepate it for fetch call\n    return fetch(this.getCallConfiguration());\n  }\n  ...\n}\n\n```\nAlright, now it looks pretty straightforward.  ApiCallConfigurationObject contains all logic to manage values for 'state',  and then, when parent call 'perform'  method, the data from state will be gathered and converted into right format to use for fetch call.\n\nThen, as a next step, we can go furher and add manging of all user.getDetails calls on global level.\nIt can be something like\n\n```javascript\n//init.js\nimport user from 'api/models/user';\n...\nuser.global().\n  .addInterceptor({\n  \tpreCall: () =\u003e {\n    \tif (isNotAllowed) {\n\t    logger.info('API call for User data is temporary restricted.')\n\t    return Promise.reject();\n        }\n    }\n});\n\n```\nLook pretty powerful, right?  In fact, you can extend it as far as you want, because now you have endpoint to manage work with back-end. End-point which was hard to imagine with implementation when you just use \n```javascriptfetch\n('/url/id') \n```\n\ndirectly from view layer. Nice.\n\n### More complexity?\nWhat if you have a lot of APIs and do not want to be affected too much by each its change? Well, if your API is pretty stable I think you should not over-complicate things, otherwise, one more good thing for you is *data-source* layer.\nData-sources layer helps you to organize loose coupling with APIs end-points.\nRemember the implementation of user model method?\n```javascript\n//api/models/user.js\n...\ngetDetails = (userId) =\u003e {\t\n    const apiConfigCall = new ApiCallConfigurationObject();\n    \n    apiConfigCall.setBaseUrl(URL_CONST.USER_DETAILS);\n    apiConfigCall.addUrlParams(userId);\n    \n    return apiConfigCall;\n}  \n```\nAs you can see it's tightly coupled with URL_CONST.USER_DETAILS for exact data-source (like you-api-provider-for-user.com/user). \nBut what if you can process current source dynamicaly? like\n\n```javascript\n//api/models/user.js\n...\ngetDetails = (userId) =\u003e {\t\n    const currentDataSource = getCurrentSource();\n    ...\n    return currentDataSource.getDetails(userId);\n}  \n```\nAnd getCurrentSource is a method which depends on configuration return you current data source implementation.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbogdan-lyashenko%2Fdata-layer-for-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbogdan-lyashenko%2Fdata-layer-for-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbogdan-lyashenko%2Fdata-layer-for-ui/lists"}