Good question. Custom data is tricky. The first thing we always try to do is dig deep into the FHIR spec to see if the data is truly custom, or if there is an existing FHIR representation that will actually work. In many cases, there are good options, and it's nice to work within the spec.
If not, then falling back to extensions is the next logical step. FHIR extensions can be clunky to work with. Our client libraries provide a bunch of helper utilities to make it easier.
In extreme cases, we have created custom FHIR resources via custom StructureDefinition and SearchParameter resources to represent completely unique data elements (examples: https://github.com/medplum/medplum/blob/main/packages/defini...).