On the code level, it's really not all that different - the operations you perform 'directly' through function calls in a pure server render model you simply put behind another HTTP interface.
I get that this mapping work might be a bit cumbersome and take some extra work, but I'm unconvinced that it introduces some kind of security tradeoff - the client ultimately has the ability to call through to exactly the same operations, just getting the data back in a different format, right?
Instead I have to figure out what data access patterns my frontend will need and implement those, along with whatever visibility restrictions are required. I'm forced to do the former in the backend (as well as the frontend) because the latter can only be done in the backend.
(Aside: I realize that back in the day people did indeed use database mechanisms like DB users and stored procedures to do what I describe, and then client apps connected directly to the database. But this practice seems to have faxed and isn't readily doable on the web anyway).
> (Aside: I realize that back in the day people did indeed
> use database mechanisms like DB users and stored
> procedures to do what I describe, and then client apps
> connected directly to the database. But this practice
> seems to have faxed and isn't readily doable on the web
> anyway).
This is actually exactly what postgREST does for you. So in that case, you actually can eliminate the backend entirely and run the entire application in the frontend, with the security provided by stored procedures and row-level security. I think Firebase, which is somewhat more widely used, also enables a similar architecture.That said, I actually agree with your preference for server-side rendered applications. The problem is that from the browser, you can't really do anything that is not SQL -- you can't, for example, call into a C library, query a legacy API, and so on. Of course, all of that can be solved by deploying more microservices, but that not only means you have to think about API design again, it also increases operational costs. Unless you need lots of fancy live-updating things, server-side rendering is still fine, and even when you do need to update dynamically, there's Blazor Server now.
> "client should only be able to perform certain kinds of transactions and mutations"
The solution to these is equal for server rendered HTML containing {data} or an API just getting {data} in raw form - the request contains an access token which identifies and authorises the user - same goes for mutations on {data}.
The answer to the question 'how do you know who the user is when they request a server rendered page containing some sensitive data?' is literally the copy/paste solution to how to do authentication on your API - as in, it's basically the same code that must be implemented on the backend in either case :-)
The point is it's the same data and mutations, just a different interface over them. There is no security tradeoff of using one over the other.
No, it's not. If I expose an API e.g GET /widgets,I need to ensure that there is no combination of parameters that will ever return a widget that the user should not see (e.g because it belongs to some other department).
Whereas when I render a web page server side, I just need to write a single, correct, SQL statement that pulls in the widgets that this specific web page requires.
It's the same data, with the same auth needed. If it's accessible to anyone over the network you need auth for everyone in either model. The auth code is the same code in either model, only the interface changes.
Looking at your example to illustrate: you have user A and user B, who have permission to see different types of widgets depending on what department they belong to.
--
Scenario 1: filtering per-resource
There is a server-rendered /widgets page that lists all widgets, filtering based on what types of widgets the requesting user can see.
API implementation: /widgets endpoint that returns a list of all widgets for the client to render. It must filter based on what type of widgets the requesting user can see, so that only widgets they can see are included in the list. Same logic as above.
--
Scenario 2: grouping resources and authenticating at entry
There are different server rendered pages for each department, listing all widgets for that department: /widgets/foo-dept, /widgets/bar-dept. You must check the permissions on the requesting user before rendering the page, to see if they have access. If user A can't see foo widgets, you give them some kind of permissions error when they request the /widgets/foo-dept page.
API implementation: Add a dept param to the /widgets endpoint that lists only widgets in the specified department. If dept param is populated, check that the requesting user has permissions to see widgets in that department. If they do, proceed, if not, throw a permissions error and deal with it on the client to produce the exact same UI as in the server rendered example. Same logic as above.
--
If you have resources accessible over the network, and you do auth on them based on user attributes, you have to do that auth either way, regardless of if you're server rendering the data into HTML pages or returning it as JSON or whatever else and rendering the HTML on the client.
The goal of both applications is to display HTML. The duplicated work in an SPA is in the DB API -> JSON API and JSON API -> page steps. These steps involve telling my backend server about how my data are structured and how to interpret them, translating them to JSON (or something else), then telling my frontend the same things. Every SPA I have seen involves doing this work, although some (like Meteor) are designed to reduce it as much as possible. That's the standard work involved in an SPA.
The second half of my point is that you could remove this duplicate work but doing that gets rid of any security. Why is this? Let's look at my SQL API example again. In this example, I have effectively eliminated the duplicate work of making my app server aware of the data's schema. It just runs a query, serializes the blob that comes back, and returns it. So why not do that? Obviously because now I'm trusting the client too much. In a non-SPA world it's fine to trust the entity making queries (for the most part...let's not get derailed). In the SPA world it's not, so we inject the app server in between to do mediation, and we pay the cost.
In my experience, it hasn't been the case: designing a REST API (I know REST complexifies the API compared to tailored RCP) that behaves properly for the client is magnitude more work than rendering HTML with data server-side.
The main difference is switching the question from "what does this page needs to render for this user", to "what are all the things the front-end might want to access through this API for this user, and how do the front-end plan on filtering". The former is obviously easier to answer, and as the result easier to code.
Honestly most of the time you can even just cut out the REST layer except where it overlaps with views by coincidence. For instance in practice there are quite a lot of views which simply get a single resource, or list a single type of resource with paging.
For mutations I am a big fan of just doing it RPC-style. I know it's not to everyone's taste but I feel like abstracting mutations out behind a model of RESTful resources and verbs is, 99% of the time, a waste of time, the notable edge case being where you truly need a flexible API for 3rd parties (both apps with UIs and scripts) where organising functionality in this completely generic flexible way - with the considerable extra effort that you mention being the price to pay - has huge payoff.
100% agree. But that (authentication) is not what this thread is about.
> The point is it's the same data and mutations, just a different interface over them. There is no security tradeoff of using one over the other.
100% agree again. A properly written application makes no security trade-off here by using an API. But that's because a properly written SPA stack does the work of interpreting the data and it's relationships twice on both the frontend and backend.
This section also frequently applies to security.
> but still inflexible enough to prevent security holes.
Unless it involves a transaction or government secrets, security is usually way down the list of priorities.
If I'm able to render things from the backend, I just have to query the data and slot it into the page. No need to design an API that's flexible enough for application use but still inflexible enough to prevent security holes.
Also, if I decide later to update how the data is represented (a common activity in early-stage projects), I only have to edit only one thing instead of two things.