is there a best-practise for this?
One of the main things to consider is the volatility of the data, i.e. how prone it is to change.
In decreasing order of volatility, solutions can range from:
- Always fetching the values from the backend.
- Fetching and caching the values on the page for future reference (more volatile => shorter cache expiry)
- Fetching and caching the values on the browser's local storage for future reference between page loads
- Hardcoding the enum values in your frontend
It should be noted that while this bullet point list is written in decreasing order of volatility, it is also written in increasing order of performance and efficiency.
You really need to balance your approach here because erring too much on either side is going to negatively impact your end result. Either you waste performance by doing too many calls to the backend, or you fail to respond to changes quickly enough by not doing enough calls to the backend.
In general, I would favor lowering the quickness of your response as much as you can get away with (without upsetting the customer). One thing we found out is that customers are happier if they might run into an outdated list once in a while (we gave them a manual refresh button to fix it); compared to always having a slight performance issue.
should I use a GET-endpoint to fetch the lists at all, or should I just hardcode them in the frontend?
Unless your data is highly volatile or you have a concrete need to avoid small redeploys, I would suggest that you consider hardcoding these values. Barring the two things I just mentioned, the benefits really outweigh the cost, by a (IMHO) large amount.
The first example I added below specifically addresses how to go about hardcoding them in the frontend.
Store the lists as a key-value pair, i.e. gender ('Male' 0, 'Female' 1, 'Unisex' 2) and jobs ('Student' 0, 'Teacher' 1, 'Engineer' 2, ...)
What you are describing here is an enum, and you might be better off working with enums instead of hand-crafted key-value pairs.
backend only stores the id, mapping from id to value will be done in the frontend with the fetched list
I would offer that for certain option lists, especially low-volatile ones like gender, both your frontend and backend should make use of an enum, not a magic number. More on this solution later.
have an own GET-endpoint for every list (gender, jobs, ...) to fetch the key/value mapping
Again, this depends on the volatility of the data. However, if you can assume that within a single page load the data is constant, and you're dealing with many small option lists, it might be better to merge the calls into one. This cuts down on the overhead performance cost of each individual network call.
I would, however, be inclined to expose both an individual endpoint and a combined endpoint. When you have one already, it's a negligible development cost to provide the other; but doing so gives the frontend the freedom to decide whether it wants to get everything or just some specific thing.
The rest of this answer lists a few ways I've seen this tackled. In all cases, it's a matter of tailoring your approach to your requirements.
1. Hardcoding the enum
I'm specifically focusing on your gender example here, as it's ripe for being cast into an enum type. Gender is not volatile enough to warrant a repeated interaction between back- and frontend. While you may expand the options at one point, it won't be frequent.
gender ('Male', 'Female', 'Unisex')
Hardcoding your options into an enum type may sound like it's a naive implementation which lacks flexibility compared to the other options, but there are some real worthwhile benefits to this.
- Performance. No extra network calls needed.
- If your frontend uses a typed language (e.g. TypeScript), having an enum allows for nicer syntax and dev support, compared to always having to work with a list of unknown string values.
- I really want to stress there that there is a big impact on your code readability when moving from an enum (where you can explicitly reference its values) to a list of unknown values (where you are always working with abstract values and can never value match anything).
- Presumably your backend will be making use of this same enum type. Depending on your backend language, you may be able to share the same enum declaration, ensuring that your back and frontend are always in sync with one another.
Even when the backend is written in a different ecosystem and cannot share the same enum declaration, it is possible to set up a template generation between the two.
I've used this with great success in the past. Whenever I'd rebuild my backend, my solution automatically built TypeScript services which consumed all my available API endpoints, and in the process also serialized all used types (models, enums, ...) which were used by the API (both input and output).
The end result here was that I never had to write any API-handling logic in my frontend. I'd make a
PeopleController in my backend with specific endpoints, and then after building the solution, my frontend would already have a
PeopleService (with an already implemented method for each endpoint) and a
PeopleModel DTO ready to go.
The downside here is that you need to redeploy the application to update the enum. However, for low-volatility data, this is a negligible cost. If you are working in a CI/CD context, needing to deploy isn't a big hurdle to cross anyway.
2. Only fetching the difference
I once worked for a company who created the mobile apps that delivery drivers use to deliver parcels (you've probably had to put your signature in one of those apps).
This entailed a vast amount of configuration options. I'm not talking about the parcel data, but the application configuration data. One example here is that if a delivery could not be finished correctly, the delivery driver needed to log what was called an "incident", but he could only choose from a preset list of possible reasons. This list was frequently updated when new exceptional circumstances arose.
However, due to a lot of complex business logic revolving around the incident types, we didn't want to keep fetching the whole list over and over. It would be megabytes of data, for tens of thousands of devices all logging on at roughly the same time every day.
Due to the need to cut down mobile data usage as much as possible, and the fact that the devices needed to keep working if they lost connection, we inevitably had to store the data locally. Which inherently begs the question: when do we update this local cached data? How frequently? How much bandwidth will this cost?
When there was a network connection, once a day we would poll the backend, and ask it to send only the incident types that had been updated or created since [timestamp]. The value of [timestamp] was given by the device, i.e. the last time this synchronization happened. This ensured that e.g. a new device (who had never synced before, or had corrupted its cache) was able to get everything, without us needing to develop a second endpoint for this).
This takes a bit more effort to implement. You need to develop a custom endpoint, track all of your entities' create/update dates, handle special cases such as deletion, and the mobile app needs some custom logic to apply a partial update to its local storage. This was a non-negligible development cost.
However, it cut mobile data usage by 98.3% (across the entire backend, not just the incident types endpoint!). It was so very wasteful to load 100+ complex objects when likely only one or two had been added/changed since last time, that the company saved more on its mobile data cost than the development of the required logic.
This specific solution may be an overly involved solution for your particular problem, but it highlights that you really need to balance your desire for quick response to volatile data, and your desire for overall performance and network load.
- Maybe you have ample bandwidth and would prefer to cut down on development costs.
- Maybe you have limited bandwidth and would rather spend effort developing a more efficient solution
- Maybe local performance is paramount, no matter the bandwidth or development cost (this is often the case for games).
- Maybe you hate having to debug or otherwise deal with caching issues and would rather suffer a slight increase in bandwidth usage.
- Maybe you want to cut costs across the board and your users having an outdated list once in a while is not big enough of an issue to spend time and effort on.
The point being that you have to pick your poison: many network calls, or the possibility of outdated data, or the added development cost of avoiding the former two at the same time. Depending on your requirements and scenario, one poison might be easier to swallow than the others.