7

I'm working on a API for the logistics department, and I have a resource called logisticTransport, which is an entity in our database. I'm facing a challenge with maintaining consistency when retrieving data from this resource.

In some parts of our application, we only need to retrieve certain fields from logisticTransport, so we return a subset of the fields. However, in other cases, we need to retrieve the full entity, including related fields from associated entities.

This approach is causing some inconsistency because logisticTransport sometimes contains only basic fields, and other times it includes additional related fields. It has also become challenging to synchronize these different types of responses in our codebase.

For example, I have an interface in TypeScript called LogisticTransport, which represents the type expected by the frontend. This interface includes the basic fields. Should I create a separate interface when I want to retrieve the full LogisticTransport resource with all the related fields? Or would it be better to use a single interface that includes all possible data, even if some fields are not always returned?

How can we structure our API to ensure consistency across different use cases while allowing flexibility in what data is retrieved? Are there best practices for handling this kind of scenario?

Thanks in advance for your help!

2
  • would optimistic concurrency help here? en.wikipedia.org/wiki/Optimistic_concurrency_control
    – LLL
    Commented Aug 8 at 16:53
  • 1
    @LLL: optimistic concurrency helps with data manipulation when multiple people can edit the same records. This question appears to be about reading data, not writing it. Commented Aug 13 at 12:16

4 Answers 4

6

There is no general rule you can apply for this scenario that works universally across all applications. The problem with adding everything to the LogisticTransport interface is that there is no clear indication whether you have the full information or the partial info. Since you have TypeScript available, I recommend two interfaces with good names. The hard part is naming these silly things and defining the scope of what they contain.

If the first API response is a subset of the full information, I recommend one interface for the partial information and a separate interface for the full information. I'll take a stab at naming things, but you can change the names to suit your needs.

  • BasicLogisticTransportInfo — this would define those minimal fields returned by the partial response.

  • FullLogisticTransportInfo — this interface has everything from the full response.

You want the names of these interfaces to communicate two things:

  1. They contain information about logistic transports.
  2. One contains partial information, and the other contains full information.

These could be two separate interfaces with no inheritance relationship provided you never need to pass the full information to a method that only requires partial information. This option is nice because the API responses for the full and partial information are free to evolve independently without requiring changes in the frontend code. The downside is that you need to cast or convert the "full" information to the "partial" information in cases where you need to pass the full info to a method that only requires partial info.

You could have FullLogisticTransportInfo inherit from BasicLogisticTransportInfo. Now you can pass a FullLogisticTransportInfo object to a method that requires a BasicLogisticTransportInfo object without casting or converting. The disadvantage here is coupling the two API responses together. A change in either API response could bleed over to the other interface causing downstream changes in other frontend components.

Choosing inheritance or two separate interfaces will largely depend on whether you believe the partial response will continue being a subset of the full response.

You could look into composition to separate things as well. Rather than two interfaces on the frontend, you can have one LogisticTransport interface. The partial information would be defined on this interface. Additional information provided by the full API response would then be decomposed into other interfaces which are referenced as properties of LogisticTransport. Unfortunately, I cannot give any more information about this option without knowing what each API returns. Composition might not make any sense from a REST standpoint, which means the API response would need to be restructured on the client rather than just being blindly passed along to the caller.

9
  • Typescript is structural enough that FullLogisticTransportInfo is a subtype of BasicLogisticTransportInfo without the definition of full knowing that basic exists
    – Caleth
    Commented Aug 9 at 9:10
  • 4
    @Caleth: It doesn't matter what Typescript does or does not, it's a modelling issue. If you add a field to BasicLogisticTransportInfo that is not populated in "full" responses, you now are in trouble. Whatever language you use, the "is-a" relationship is now broken. Hence making "basic" a super type of "full" has a long lasting impact, not to be taken lightly. Commented Aug 9 at 10:16
  • 1
    @MatthieuM. What I mean is that Typescript defines subtypes structurally. So long as Full has compatible members with Basic, it is a subtype, so the conversion is the identity function. Yes, if you make an incompatible change, it breaks loudly.
    – Caleth
    Commented Aug 9 at 13:32
  • @Caleth: Ah, I see. So you have the conversion for free, without inheritance. That's pretty good. Commented Aug 9 at 13:34
  • @Caleth, that depends on the code being executed. I thought I found cases where the implicit conversion wouldn't happen. Basically if the object was declared in the same function then it would do the implicit conversion, because the TypeScript compiler can verify the exact properties in the real object. If you get an object as an interface and then pass it along to another method expecting an unrelated interface, then I didn't think TypeScript allowed for that. My memory is a tad fuzzy on that one, though. Commented Aug 9 at 13:50
5

The easiest and best solution is to return the full object with all fields all the time.

This avoids a coupling between the Model and the UI, or whatever process you are creating and allows the same API to be used across multiple consumers.

In most cases the argument that less data will be used if you limit yourself to a "lite" object is untrue. It neglects to take into account caching and reuse of the object. For example you might have your list page, and an item detail page. You save on the list page by passing fewer fields, but then have to request the same data for the detail page. Where you could have just used the already downloaded object if you had sent full data the first time around.

The downside to this approach is that it forces you to be careful when defining you Models. You can't have Company.Employees[400].Address.Street. The Company object would be too big. You have to have Company and Employee with Company.EmployeeIds.

4
  • 3
    There is something to be said about not making things different unless you need to. Commented Aug 8 at 17:42
  • There is another downside to this, though not as bad as in times past - more required bandwidth. If the full information is truly not used, it is effectively wasted.
    – trysis
    Commented Aug 9 at 18:11
  • 1
    That’s where you measure. If it’s 1kb vs 2kb. on an internet connection, or 100 vs 200 kb on an internal high speed connection then it doesn’t matter (most likely).
    – gnasher729
    Commented Aug 9 at 18:47
  • On the other hand, you can split information over databases. Say an invoice has an invoice Id, a customer id and a list of product ids. You transfer the complete object but not the products.
    – gnasher729
    Commented Aug 9 at 18:50
4

What you should implement here are API filters. Then the HTTP Clients can request specific data as they need.

Without filters and returning pre-defined data sets you are serving the Client's needs too tightly, and this can become messy with each Client's specific needs. E.g. what if next week the Client wants a 3rd option, then later another client wants some kind of hybrid between them all, and so on.

Without a filter, the API should return ALL data and the Client just use what it needs from that. The Client can implement it's own DTOs to map specific scenarios. So e.g. have the full object of all data, then some mapper to make BasicDto.

But again filters is the way to go here imo.

2
  • 1
    This is the approach I'd take. Popcorn is what I use for ASP.net APIs to solve this. The client makes queries and provides an include parameter to dictate which fields on each entity is included. Eg: GET http://myservice.com/api/logistics?include=[thing1, thing2, relatedobject[ id, thing1, thing2]]. GraphQL does this by default as well, but that'd be moving away from REST.
    – jmathew
    Commented Aug 9 at 17:23
  • 1
    OData also has the same concept.
    – jmathew
    Commented Aug 9 at 17:36
4

The fundamental thing to avoid is overwriting a fields good data, data that wasn't downloaded, with a null (or empty string). A null that only exists because the field wasn't downloaded.

The problem with encoding the "don't use the data in this field" signal as a null is that sometimes you want to overwrite good data with a null (or empty string) because it's time for that data to go away.

Well, that, and some fields just can't be null.

This problem can tempt you to split the interface into basic and full varieties. Which will work well right up until you change your mind about what field goes in which.

There is an alternative. Stop insisting this is a type. Treat it like a data structure. JSON is really good at showing you what field wasn't downloaded by simply not including that field. Collections are also good here.

Static types are not good at representing partial data. Data structures are. The rub is now you have to code against a data structure.

That's the most flexible solution. If you don't need that kind of flexibility and you're confident you know what basic and full will be now and forever then stick with static types.

If you insist on static types (because you hate data structures that much) and max flexibility then you're simply going to have to find somewhere to encode that "don't use the data here" flag.

3
  • Thanks for your answer. I am curious about the differences between static types and data structures in this context. Could you provide an example or any references? I can't imagine how I could apply this approach of using data structures instead of static types defined in both the front-end and back-end.
    – danidcode
    Commented Aug 9 at 10:56
  • Retrieve fields StrictTypeA.x & y but not z. Your retrieved json/collection has no z. Work with retrieved x & y. Change retrieved x. Decide it's time to update StrictTypeA. Send your updated json/collection back to it. It gets a new x value, same old y value (since you didn't change it), and StrictTypeA.z goes untouched since z is not in the json/collection. Commented Aug 9 at 11:06
  • Now code that works with retrieved x & y could invent StrictTypeB that only has x & y. But this is NOT the same as the partial type idea. No here StrictTypeB exists to say that's what this using code needs and doesn't care what it's part of. Which means it's something else's job to worry about keeping the json/collection it produces compatible with StrictTypeA. Commented Aug 9 at 11:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.