lwc dataservice architecture best practices

Firstly, a very nicely framed question!

Here is what we found to be best after brain-storming:

  1. Apex methods should be used ONLY for getting the data and for DML statements. In short, it should be used only as a communication layer between component and database and nothing more - no more modification of data in apex. All the modifications to the data-structures should be done in client side.

  2. There should be a single source of truth to data - always. So, you should be getting the data in the parent component, then do the necessary modifications to the data and create an object something like:

    this.mainData = {
        actualData: [{},{},...{}], // data from server
        tableData: {
            columns:[{},...{}],
            otherAttributes: {}
        },
        mapData: {
            someAttributes: {}
        }
    }
    
  3. You can pass either the mainData or the needed data like mainData.tableData to child components.

  4. Whenever any data change is made in child components, send that change in custom event, and the parent component should handle it thereby automatically passing the data down the hierarchy. Remember that in any case child components cannot modify api properties, they should be working on the cloned properties.


Child component is not self-contained. You always need to provider a higher order components when you try to use the child component.

Not all components can be self-contained completely. They will be either data-self-contained or UI-self-contained. So, this is totally fine in terms of scalability and readability.


DataModel VO and converter is in javascript and it is not type safe. Decreasing the readability of the code(maybe)?

When you are directly returning the database objects, there will be no problem as you have to use the API names of objects/fields everywhere in client side HTML/JS.

But when you have to get the data from multiple sources, you can create a separate class. This class will have all the properties needed and separate methods to define each data-type. Consider below class:

global class pocMyData {

    @AuraEnabled global String Id{get;set;}
    @AuraEnabled global String accName{get;set;}
    @AuraEnabled global String conName{get;set;}
    @AuraEnabled global String description{get;set;}
    @AuraEnabled global String datatype{get;set;}
    @AuraEnabled global String otherField{get;set;}

    public static pocMyData getMyDataType1(sObject sobj, sObject otherObj) {
        Account acc = (Account)sobj;
        Contact con = (Contact)otherObj;
        pocMyData pocInfo = new pocMyData();
        pocInfo.datatype = 'accMain';
        pocInfo.Id=acc.Id;
        pocInfo.accName=acc.Name;
        pocInfo.description=acc.description;
        return pocInfo;
    }
    public static pocMyData getMyDataType2(sObject sobj, sObject otherObj) {
        Account acc = (Account)sobj;
        Contact con = (Contact)otherObj;
        pocMyData pocInfo = new pocMyData();
        pocInfo.datatype = 'conMain';
        pocInfo.Id=con.Id;
        pocInfo.conName=con.Name;
        pocInfo.description=acc.description;
        return pocInfo;
    }
}

Here I have the ability to have 2 data-types from the mix of Account and Contact. So when I try to get the datatypes by using:

Account acc = [SELECT Id, Name, Description FROM Account WHERE Id='00128000009j45sAAA'];
Contact con = [SELECT Id, Name FROM Contact LIMIT 1];

System.debug('getMyDataType1 => '+pocMyData.getMyDataType1(acc,con));
System.debug('getMyDataType2 => '+pocMyData.getMyDataType2(acc,con));

I get below:

getMyDataType1 => pocMyData:[Id=00128000009j45sAAA, accName=University of Boston, conName=null, datatype=accMain, description=University of BostonModified from code, otherField=null]

getMyDataType2 => pocMyData:[Id=00328000008ZUISAA4, accName=null, conName=Rose Gonzalez, datatype=conMain, description=University of BostonModified from code, otherField=null]

If you observe above, I know from datatype whether its accMain or conMain. In this case, the properties will become the API names for the client side components.

Now, when you convert the accounts and contacts using this global wrapper, your code will be readable and error-free in client side as the API names have single source of truth.


So, I was struggling with this too.
I had a record that I was passing around which was being used by several components, and I didn't want to rewrite the same helper code in multiple places. I also didn't like having to document all the elements of this record in each of my components that were using it (since when I made a change, I now had to go to each of those components and make the change in the docs) Also, there was a number of common methods I was using to handle this record in each component that I didn't want to replicate:

I created a Javascript service class ("SomeService.js"), much like we commonly do with Apex. First I included a constructor whose Input Arg is a result from an Apex method. Now any component can construct the same object using the same Apex input. Next, I added some "private" methods that were used only in the Service class. (You'll see at the end, that the private methods are excluded from my "Service" object) Then I included other methods that were for local and public use by other components. Now all my various components can be working with the same record(s), and execute the same methods on that record and the code is not replicated. Note the "public" methods are all declared in the "SomeService" const, which is exported. Now, all I need to do, to use any method in this Service class, is to put a single import

import { SomeService } from 'c/someService';

And I can refer to any method in my class with the exported prefix: as in:

let myObject = SomeService.constructMyObject(apexResult); 

Joila! No more code duplication and one unified record type shared by existing and future components. This helped me organize my code very efficiently.

someService.js:

        const constructMyObject = (apexResult) => {
           let myRecord = {
                    "prop1"             : apexResult.prop1,
                    "prop2"             : fooPrivate( apexResult),
                    "prop3"             : bar( apexResult.fieldx),
                    "etc"               : "Default"
                }
                return upload;
        }       
        const fooPrivate = (something) => {
         return 'foo'; 
        }
        const bar = (somethingElse) => {
         return 'bar';
        }
        const methodX(myRecord) => {
         localRecord = {...myRecord};
         // Do stuff to localRecord
         return localRecord;
        }
   const SomeService = {
      constructMyObject: constructMyObject,
      bar: bar,
      methodX: methodX,
   }

   export {SomeService};