7 ms·
One extra thing I have confirmed after writing this article: it's usually a bad idea to reuse back-end data in multiple places on the front-end. If you think a
by hakunin 3y ago
One extra thing I have confirmed after writing this article: it's usually a bad idea to reuse back-end data in multiple places on the front-end.
If you think about functions or object constructors in general, it kind of makes sense. One of the most important architectural practices I've ever discovered is: don't pass in the parameters that you have, pass in the parameters that the function needs. So if you have companyName, and you are constructing a page that has a title, you shouldn't pass it companyName: companyName, you should pass it pageTitle: companyName (parameter name: parameter value). It's crucial that the parameter is named after what is needed, not what you have. This is kind of the essence of what the article is suggesting.
If your front-end is reusing a lot of fields from back-end, it's likely that you're passing it what you have in your database/resources, and not what it needs — fields for constructing the UI, configuring the views. Moreover, I rarely see front-end going through the exercise of defining exactly what they need. The article encourages more of that.
Once the front-end starts reusing the generic back-end fields everywhere, you lose track of your use-cases. Everything you ship from the back-end becomes potentially important for mysterious reasons that not even front-end can easily understand anymore. Front-end already built complex dependency trees based on these generic fields. If you actually untangle these trees, it might turn out that your front-end can only exist in 5 different "modes", but you can no longer see that in the tangled mess. You can no longer streamline and optimize these 5 configurations.
So to summarize: pass what is needed, not what you have is a good general architectural rule that's also applicable between back-end and front-end.
- appplication 3y agoThis is really timely for me, thank you. I’m ignorant when it comes to web dev best practices (data engineering background mostly) but find myself suddenly having created a couple small backends for some of our internal (non-customer facing) tooling, and running into basically all of your questions. One of these I struggled with right away was “how do I ensure the front end and back end have consistent data sourcing and validation”, e.g. for various forms, dropdown selectors, etc. I have found surprisingly few resources for general patterns here. So I did your anti-pattern of having an endpoint for each little thing (e.g. /api/v1/country-codes or something like that). This has felt… ok at best. One challenge, perhaps entirely imagined, is coherent namespacing. Part of the backend is also a public (internally) API. We auto-generate all our docs with swagger, and I was finding that the few meaningful endpoints I had were diluted by the mess of individual endpoints for the frontend. So I threw these under /api/v1/config. Feels like a junk drawer but at least it cleaned things up. I like the idea you’re proposing here for having one API per page, I think it makes a lot of sense. It also eliminates the “I guess I’ll add another endpoint” and changed it to “let me add another field”. Feels more granular and scoped. I will be giving this a try!
- nonethewiser 3y agoI don’t think what he’s suggesting is common whatsoever. Take that however you want.
- appplication 3y agoBecause I’m otherwise uninformed to pros/cons here and don’t have the benefit of experience to fall back on, what are the alternative suggestions for best practices for these types of problems? For context, my app is a single backend dev (me), separate frontend team that tasks on as needed. Backend dev resources are unlikely to expand beyond me for the 2-year horizon. I also have all context on vision for direction of the web app, and field all user feedback and requests. Front end team has no context on user needs, and does not keep any roadmap for the app. So it seems like the above recommended pattern could make sense in that I could drive more front end work from the backseat, so to speak.
- 8bithero 3y agoGiven that you're the sole backend developer with a focused vision for the app, the Server-Informed UI approach described in the article could work well for you. It simplifies the backend architecture and makes it easier to coordinate with your frontend team. However, for long-term scalability, you might run into issues with this approach. If so, take a look at "Bounded Contexts," a technique in Domain-Driven Design that helps break down your application into manageable, scalable units.
- theptip 3y agoI think the standard evolution of an API is: 1) basic CRUD API for your objects (eg DRF generated model-API in Django). Really easy to build. Client is assumed to have some magic knowledge (eg which country code style you use). Ideally as much of the smarts as possible is server-side, but early on you can compromise on this. 2a) for reads, notice you need nesting / joins, add nested fields. Maybe notice these joins are slow and make them optional; build some expanded=true or with_subobject=true type query params. 2b) for writes, notice that CRUD is getting hard to maintain as frontend can create n^2 possible states for n fields, and backend validation needs to consider all other fields for each update. Move complex state transitions to mutation/action/verb endpoints instead of PUT(any fields). Add your first “status” fields. This gets you quite far, likely to be fine for 2 years and beyond. After this there are lots of options depending on needs, and overhead associated with more layers/abstraction; 3) use something structured like GraphQL, or just refactor the API to do away with directly updating objects. Maybe lean into Finite State Machines if you have a small number of objects with complex transitions, endpoints trigger those parameterized transitions. Another option to consider is a server-side rendered framework with client-side hooks like Django+HTMX, Rails Hotwire, Elixir Livewire, where the frontend devs are working on styling, design, templates, and most/all the actual app logic is backend code. This gets you surprisingly far these days. If you are the one defining all the features, you’ll iterate quicker in this mode.
- jlawson 3y agoIt's an example of the general rule to name/implement functions according to what they do, not how they're used. Stated another way, when building a tool, don't let information about the incidental context leak into the design of the tool itself. It should be designed to serve its intended purpose independent of when, how, where, or why it was built. This way, it won't break when that context changes, or when it's used in a new context.
- xpe 3y agoI was nodding along until this part: “It should be designed to serve its intended purpose independent of when, how, where, or why it was built.” This is impossible and in many cases undesirable. As long as the purpose and behavior of the function is clear, it is OK if it has contextual roots. Actually, no, it isn’t just OK, it is the way it must be. Take one aspect of a sorting function for example. Should it be stable? Or not? This decision is rooted in the context. To try to make the function independent of context is impossible and unwise. Are you making a different point?
- adammarples 3y agoWhether it's stable or not is irrelevant, if it's "stable_sort()" then it always should be, if it's "sort(stable: bool)" then it could be. It shouldn't depend on why and when I intend to use it. Most importantly it shouldn't suddenly become stable when I pass "sort(companyName=Walmart) because that's what we needed to do for Walmart but none of the others. That's outside context leaking in and a big mess to maintain.
- xpe 3y agoThe discussion here is rooted in how we understand ‘context’. Your comments about stable sorting are well and good, but they don’t disprove what I was trying to get across with my example. Why a ‘business logic’ function uses a stable sort (or does not) is contextual. I understand your point, but do you understand mine? This is just one example. The idea for mutually beneficial communication is this: one person strives to be charitable and understand another’s example as a way of understanding their point. This is not a skill necessarily taught or nurtured in the field of software engineering, but it should be. Most of us have the deductive/analytical skills to use an example to prove our point. It is actually harder to take someone else’s example and see their point. It is harder empathetically and computationally; it requires some inductive steps. (I think there’s a connection between empathy and generalization/induction, but that’s for another time.) My comments are obvious — almost tautological — if you think epistemologically. Programs and functions are designed to solve problems in context. You can’t get around this fundamental truth. Philosophy has been around longer than programming; in fact, it is one of the historical roots of mathematics and programming. Discussions of good design, which may involve parsimony, orthogonality, and more, have been discussed at length well before software existed. We’d be wise to not ignore lessons already learned. Such is my experience, having seen and learned a lot from a lot more than software. Claiming that something is context-free is a bold claim. Pure mathematics and logic are probably the only areas than can make that argument credibly.
- nonethewiser 3y agoSeems like you couldnt have front end engineers with this philosophy. Or you could but it would be very unproductive. Because theyd need backend changes for every API call.
- paulddraper 3y agoThis philosophy encourages full stack engineers. So every feature/change only needs one engineer.
- hakunin 3y agoWhile this may be true to some extent, I think front end has plenty to deal with even if you do ship them the exact data they need. Primarily, this philosophy encourages you to maintain a strict connection between needed use cases, and the code that enables them. Once that connection is severed, your code expands beyond the required use cases, and you end having to maintain support for phantom/theoretical use cases.
- 8bithero 3y agoThis approach would also be a nightmare if you ever decided to refactor the frontend. You'd be forced to do double the work since failing to update your backend would probably result in nonsensical legacy naming... I'd also dread to think what would happen if you also used the backend to serve a mobile app. You'd end up spending 40% of your time trying to figure out what to name fields before eventually going full circle and reverting back to logical generic names :|
- rvnx 3y agoAnd the moment your SaaS is growing, and your customers request access to your API: Instead of just implementing ACLs/permissions on existing APIs you have to develop and maintain two APIs, one for the customers (that will get outdated with lot of missing features and be less battle-tested) and one “real API”.
- ericmcer 3y agoI… don’t like this. You are basically saying we should take a name that conveys info about its content (companyName) and change it to something that contains none (pageTitle). I can tell if a field is being used for a page title by looking at the html, I can’t tell what it contains unless it is named well. Additionally if I ever want to hop into the backend my time on the frontend will have given me 0 context if you are renaming all the fields. Also if you want to use Typescript this would prevent a bunch of automated type generation you could do.
- jackblemming 3y agoLike most things, it depends. If you have a generic math module, you of course want generic parameter names. It would be weird to have a differential math library with a bunch of random terminology from another domain mixed in. If the page only makes sense if a company name is used as a title, then of course use companyName as the variable.
- rvnx 3y agoIt’s a very curious way of doing things. The result with this strategy is that you may need the companyName and end-up at some point with code that does back: companyName = pageTitle or worse: companyName = pageTitle.split(‘ - ‘)[0] Because there is a moment when you will need to get the companyName again for some comparison or some whatever logic (e.g. is selected account profile current company ?) This feels very confusing. Instead pass companyName and keep it that way until the very last moment that you display it on the page (e.g. when rendering the view/template)
- hakunin 3y agoThe whole point is that when you need companyName for another thing, you should create a parameter `anotherThing` and backend will set companyName into it as well. It's designed to make it very weird to _not_ do that, so that you don't do that.
- deleted 3y ago[deleted]
- kdazzle 3y agoI don’t know about this. I do think people tend to prematurely optimize by over-generalizing their APIs instead of building what they need. But this seems like it swings too far the other way.
- jmilloy 3y agoThis and the article strike me initially as completely wrong, which makes it very interesting. I still want a clear separation between the "model" and the "view", and I guess I tend to assume that this corresponds to the front-end and back-end. Maybe that's a poor assumption. What I see here is that the "view" is split across the front-end and the back-end. Or in other words, the back-end contains both the model and the JSON API as a view. I think this is what is meant by "I suggest you stop treating your frontend as some generic API client, and start treating it as a half of your app." I still want a clean separation between the model, e.g. companyName, and the view, e.g. pageTitle. Somewhere, there needs to be the explicit logic (in the back-end part of the view) that says that the the pageTitle for page A is the companyName. It's okay for many different views to "reuse" fields from the model, but each view should have its own front-end and back-end components. That the view is split across a front-end/back-end divide through the nature of the web-stack doesn't change the fact that each view should remain distinct from the other views. So I can agree in the sense that the alternative where you create a general-purpose JSON API as a model and view in the back-end, and then essentially code up an additional model and view for each front-end page/element with logic to convert the API data into the necessary content is a waste of time.
- hakunin 3y agoArguably, the opposite is true. The view is more washed out across the stack if back-end supplies generic fields, and front-end decides how to use them. Now you no longer know who is responsible for which decisions, and have to look for them everywhere in the stack. If back-end provides the entirety of data to build the page, then you limit front-end decisions to UX based on the specific data, and no longer allow them to pull in new functionality or foundational business logic over to their side.
- Terretta 3y ago> arguably... view is washed out across the stack Perhaps to argue this requires munging "view" with "front end"? MVC needn't mean database schema, front end, and logic. It could, but then what is a "view" in the database? Perhaps you're reinventing MVVM? https://en.wikipedia.org/wiki/Model–view–viewmodel https://en.wikipedia.org/wiki/Model–view–viewmodel
- simplify 3y agoFull static typing alleviates this greatly, allowing you to pass in what you have now and decide to refactor later if/when you need to. For example, if you remove a field that's still needed, you'll receive a type error, eliminating the problem of losing track of use cases.