Basic Single Page Application (SPA) Architecture

Data Access Layer

Michael Timbs
JavaScript in Plain English

--

Zentrum Paul Klee Museum by Ricardo Gomez Angel

There’s no incredible insights here, no hot takes and nothing worthy of applause. It’s a simple rule that I follow on every SPA I build — regardless of size. To me, this is obvious and make sense. I cannot conceive of maintaining an application without having a dedicated Data Access Layer (DAL). I consistently see the same problems occur when people access API’s directly from their components. A DAL is a titanium bullet solution (silver bullets don’t exist) that squashes all of these issues at once.

Data Access Layer may not be technically the correct term, but the interpretation in English makes sense enough that it’s what I use, so sue me if I’m wrong.

TL;DR: Build a dedicated and shared data access layer for your components. Never access an API directly from a component.

Let’s take a look at these trivial Vue components so that I can illustrate my grievances.

If you aren’t familiar with Vue, our first component fetches a list of pending “todos” from our API in the component’s mounted hook, set it as a data property and then iterate over the collection to display a “ToDo” component in a list. The next component does pretty much the same thing but fetches recently completed todos. Finally, we have a component that fetches a single todo based on an id.

None of these issues I mention here is unique to Vue, and I see them just as frequently in React. Angular isn’t my jam so who knows what goes on in that world. Svelte and Elm are just too pure to have these issues 🤘

Reasons this code makes me want to cry

Structural Coupling

(Unnecessary) Coupling is generally bad. Avoid it wherever possible. I bet you’ve never heard this before. Stop for a second and appreciate this profound statement that shakes you to your core and makes you question everything you (think you) know about building software.

Structural coupling isn’t always a concern, but it is by far the most ubiquitous and pernicious issue I see.

On the surface, this coupling seems pretty harmless, especially if any of the following applies:

  • You have shared type definitions between your SPA and API (actually relatively rare in my experience for large projects) which mean you get good type safety and consistency on data structure
  • You have a stable API that rarely changes and have good contract testing in place

Structural coupling makes it very difficult to make changes to your API contract. If you want to rename a field or change the structure of your response, we now have to update it in all three components. If you are using TypeScript with shared type definitions, you might pick this up with static analysis and avoid shipping broken code. It’s still going to be a pain to fix — in practice, it might be 20 components and not 3 that need changing.

I consider this coupling to be pernicious because it has a subtle problem that people familiar with the concept of a Ubiquitous Language might recognise. Language and context are essential, and there’s no reason to believe that your frontend and backend can’t benefit from decoupling the way they talk and think about data. The ability to rename, ignore, combine, or transform data at the entry point to your application can reduce cognitive complexity as a project grows.

Using a DAL solves structural coupling because the DAL encapsulates the logic of fetching data from an external system (your API) and transforming it into a structure that makes sense for the frontend. When we change our API, we only need to make the requisite change in a single place on our frontend — in the DAL code. In Vue, we would implement this by creating a dedicated Vuex store for Todos. Each of our three components would then fetch their list of Todos from the Vuex store.

Welcome to the world of distributed systems

As a magic full-stack unicorn 🦄, with the confidence of Muhammad Ali and the talent of McLovin, I can lecture people on frontend architecture despite spending the majority of my time on the backend.

A symptom of living in a world of distributed systems is you constantly think of Peter Deutsch and his infernal fallacies of distributed systems. Building SPA’s has similar concerns, and we need to pay attention to all of the fallacies, but the following ones chiefly apply to my current rant:

  • The network is reliable
  • Latency is zero
  • Bandwidth is infinite
  • Transport cost is zero

In our naive implementation, we are doing a lot of unnecessary work. Whenever a component renders, we are re-fetching (most likely the same) data from the API.

Even in a world of ever-increasing bandwidth and unlimited data plans, we should be savvy about how much data we need to fetch. Reducing the application data we fetch allows us to dedicate more bandwidth to advertisements and other bloatware that improves the web.

There are still places in this world with unreliable internet connectivity. While you may not necessarily serve customers in these backwater places (such as the New York subway) there are numerous ways that network reliability can be impacted that don’t involve hurtling in a steel tube under the Earth.

While latency does weed out the pretenders who never really cared about your application, it may also be costing you Twitter bragging rights due to poor lighthouse scores. The General “best practice” is to reduce latency where possible. The obvious solution is to reduce the distance data has to travel before being displayed. While I haven’t benchmarked this, fetching from a local cache should be faster than fetching from your nearest AWS data centre.

If you have a centralised DAL, you can implement caching logic in a single place. If you are getting paid by the total lines of code written, you may find this strategy to be detrimental to your bank account.

People like Walter Mischel might argue that consuming stale data now, rather than delaying gratification for fresh data, will lead your users to have poorer life outcomes. I’m afraid I have to disagree with this interpretation, and much prefer to leverage the stale-while-revalidate approach (inspired by this HTTP RFC) to serving data to my applications’ well-adjusted users.

The stale-while-revalidate pattern allows us to asynchronously fire a request to our API for new data while immediately displaying any (potentially stale) cached data we have in the meantime. On first interaction, this would be an empty array of todos. Vue’s two-way data binding and reactivity will automatically display the new data once the API has updated our cache.

This pattern allows our users to interact in a meaningful way while data loads. It should be apparent that subsequent interactions with the component(s) will reduce the severity of cache updates. This pattern allows our users to interact with cached data in the case of network failures or high latency.

The revalidate logic should take careful consideration of bandwidth concerns and not be too aggressive. You probably don’t need to revalidate all your data all the time.

The DAL cache can be as simple as an in-memory implementation, utilise localStorage or be a fully-fledged solution leveraging IndexedDB. IndexedDB is a transactional database system for the browser that lets you fetch data with SQL-like queries. It gives you a lot of power to maximise the utility of a local cache. I reach for it often, we have seen much together.

Separation of Concerns

I think about my frontend’s as nothing more than the visual representation of some shared data and a collection of interactions to mutate that data. They should be reactive to changes in the data. Since this is how I think about my applications, it follows that this is the one true way of building for the frontend.

Having a shared DAL means that my components become much more straightforward. They no longer contain any logic about data. (Nearly) All of the logic for fetching, filtering and transforming data lives in the DAL. The shared DAL allows the component to focus on markup/presentation and to manage the local state. If our components are reactive to changes in the underlying DAL (default behaviour in Vue), we get a nice data consistency throughout our application.

Testing

We’ve just punted all our data access logic to a DAL. We can now unit test our components with incredible ease. We test our components in isolation by stubbing the state of the DAL (in the case of Vue, we explicitly set the state of the store for each test). No more mocking fetch/axios or dealing with any other gross side effects in our components. Fetching data can be stubbed and we no longer need to care about testing mutations/side effects within our components.

--

--