Single-Page App (SPA) vs. Routing: the whys of each choice

What I’m trying to do:
Get design-time information.

  • When is an SPA appropriate?
  • When is Routing appropriate?
  • What factors are used to decide, and how? That is, what are the tradeoffs of each choice?

Much has been said about how to do each, but I haven’t seen much, anywhere, about why.

1 Like

Routing (at least the one that Anvil has) is still SPA. All Anvil apps are SPA (with or without any kind of routing).

But the benefits of Routing are as follows -

  1. Keep a track of where you are
  2. Make shareable URLs possible
  3. SEO of course. Search engines can index all your pages
  4. Allowing native browser back and forward navigation
1 Like

From the user’s perspective, they’re different:

  • SPA’s have a single, unchanging URL
  • Routed apps have a variety of URLs

From the developer’s perspective, they’re different:

  • SPAs are Anvil’s default. You don’t have to enable anything or incorporate any third-party libraries to make it work.
  • For routed apps, you must
    • use a third-party router, or
    • use Anvil’s, which requires enabling Layouts, or
    • roll your own.
    • Be prepared for anyone to arrive at your app from any of its URLs, when they haven’t even logged in yet, and you just don’t know who they are, nor which data is theirs.

These are some of the tradeoffs, positive and negative, I know about already. There are probably a whole host of others.

It seems to me that the entire App architecture changes when you go from one to the other. And that’s fine. But before I design my first Routed app, I want to understand the architectural and functional tradeoffs, so that I can make rational decisions along the way.

Before creating a Route, or URL, I should decide

  • what that distinct location means to the users, and
  • why they should be able to jump right to it, despite not being logged in (yet).

Right now, I don’t have an understanding of the criteria needed for making such decisions.

For a help or reference system, or anything else where log-in is unnecessary, I think I can work that out. It’s about mapping an information space and/or function space into a labelled hierarchy, i.e., an outline.

Indeed, routing does complicate the architecture. But I will still say that definitely go for routing if you want to deliver a good experience.

Anvil’s new routing system lets you define redirection logic that runs before a form loads. This lets you handle things like login checks or access control. If SEO isn’t a priority for your app, you should at least use HashRouting.

Even if you’re not concerned about shareable URLs or SEO, routing solves the key problem of navigation. Most users (especially on mobile) expect the browser’s back button to work as it does on other sites. Without routing, that back button just kicks them out of your app instead of taking them back to the previous screen.

Even I made the same mistake. Initially, I was using open_form for navigation and that made users complain that my site is unusable.

So yes, unless your app just has a single form or two, you really should consider implement routing, just for the sake of navigation at least.

Thanks! Redirection, I take it, puts them on a page other than the one they asked for. So once they’ve finished with that page, then they need to navigate to the one that they asked for. That’s okay, as long as they can get there without too much effort.

Making the back and forward buttons work is certainly a plus – for anyone unfamiliar with SPAs, at least. They’re so commonplace these days, it doesn’t bother me at all. I have an SPA with 2 megabytes of source code, several hundred “pages”, all with built-in navigation.

There needs to be an actual benefit to multiple URLs, in my work, before designing a site plan that requires multiple URLs.

There also needs to be a set of criteria, reasons why (and why not) to choose a particular structure. These are probably the most important whys in the whole design process. Without them, all the “back” button support in the world won’t help. No user will understand where they’re going, and why, if there isn’t a rationale, a literal reason why. And those reasons should be consistent across the entire site.

Another word for these “whys” is “design principles”.

Say you choose a URL for a page (of some form a/b/c/d). How did you arrive at that choice? Which factors did you weigh, and why did they matter to you?

Perhaps you are overthinking it.

All my apps use hash routing, because they are old.
All my new apps will use the new routing.

I never open a form, I always redirect somewhere. The URL (or hash) has all the info to show what I want to show, like what checkboxes or radio buttons to check, what’s on every input element, etc. The URL contains the full state of the form. If something needs to be loaded from the server, the form loads everything required in one round trip. (My next app may use KeyChain)

The main reason is that apps and other tools send notification emails with messages like “something is ready, click here to see it”, and the here has to be a link that starts the app and shows whatever is ready to be shown.

Other reasons are the management of the back and forth buttons, the “you want to save before leaving the page” warnings, the ability to copy the current URL and send it in an email.

I don’t think the permission checks you are worried about are a problem at all. All server functions need to check the user’s permissions anyway, regardless of whether it’s single page or not. If they didn’t, you would have a security hole, regardless of whether it’s single page or not.

Why choosing one URL design versus another, that’s easy: one path per form. In some cases I assign multiple paths to the same form and make the form flexible. For example I may use the same form to both create and edit an entity, but there would be two different paths. Then the form would adjust the behavior depending on the path.

Okay…

Let’s take a simple case: the path /x.

  • Is x a verb? A noun? A parameter identifying something else?
  • Whichever of the above that you chose, what information went into that decision?

I can share my experience and what I would be thinking about when starting a fresh app.

Number of user views

I found that handling multiple views while on the surface is simple in a SPA, in reality it can be a hassle. I think it is just much easier to develop multi view apps with routing. (or perhaps it just makes more sense to me and I suck at SPAs… Who knows?) As the number of views go up, the likelyhood that a user will use a “back” mouse or phone gesture to navigate goes up. So, consider what the startup costs are in the app and weigh that with the “back” navigation exit potential.

Simple or complex security policies

If your app does not need any authentication or if every view requires the same user privilege an SPA is easier. If you have a mix a security privileges, go with routing. Setting unique permissions for each page is simple in routing.

Testing

I’ve found another big advantage of using a routing based app is that testing is much easier. I can jump right to the page where I need to test some interaction rather than having to start and navigate within the SPA.

Client side data time cost

If you can just send everything you need for the app to the client at startup and the time cost is low, SPA is easier.
If the time cost is high, and your data can be broken up into smaller view-oriented groups, then routing may be a better experience for the user with faster initial page loads. This is especially true if a common user may not access particular views for a given session and you didn’t need to send down that data at all.

The other nicety in routing is the pending_form for cases where the user may need to wait. You didn’t have to really do much at all to give them a nice pending page that you don’t have to manage.

What would I do?

Beyond anything other than what I would consider toy apps, I would use routing. I would say as soon as open_form is used… just switch to using routing. The amount of extra work in setup is minimal compared to the reduction in complexity as an app scope grows.

3 Likes

That’s the overthinking part I was mentioning :smiley:

My users don’t care.
No one has ever typed the URL manually. URLs are either built in email notification links, where a more descriptive text is visible, not the URL, or copy/pasted.

I don’t care.
I look at the URL the way I look at the form constructor arguments. In some cases I put the more descriptive ones at the beginning, just because many tools or tooltips truncate the URL. For example I would rather see in the truncated URL a readable part number than other values like the text to put in a search terms input box, radio or checkboxes values, etc.

Another reason not to think too much about it, is that the URL contains a list of values, and overtime the list increases, and being too picky about the order or the meaning will make maintenance too difficult in the long term.

1 Like

two points worth mentioning

SPAs
The term SPA (Single Page Application) can be confusing in the Anvil context.

Like @divyeshlakhotia mentions, all anvil apps are SPAs, even those that use routing.
SPA just means the app never fetches a new HTML page from the server when you navigate; everything happens in the browser.

This is true of apps that use:

  • routing
  • hash routing
  • open form
Term Navigation Mechanism Dependency URL changes? Back button
Routing Path-based routing routing Yes (path) Stays in app
Hash Routing Hash-based routing anvil_extras Yes (hash) Stays in app
No Routing open_form() or replace content_panel child componets None No Exits app
  • With routing or hash routing, navigation changes the form (using open_form or content_panel.add_component(...)) and updates the browser history. These libraries also listen for browser navigation events and update the visible form accordingly.
  • But in all cases, once the page loads, you never fetch a new HTML page from the server.
  • For example, Gmail is an SPA—even though the URL changes, it never reloads the page.

Do you need to use layouts?
In the routing dependency you don’t need to use layouts
There’s a way to just replace the content_panel of the main form
(like how hash routing does things)

https://routing-docs.anvil.works/migrating/#from-anvil_extrasrouting-hashrouting

If you only use open_form, layouts are optional. Layouts are helpful for reusing components like sidebars/navbars, but not required.

1 Like

I agree that getting too worried about the path names would be overthinking, but my process is to have each part mean what the user will be seeing or doing.

For example, I have used paths like /edit/lessons/:lesson_id and /view/courses/:course_id and /edit/pathways/new (with all the parts being interchangeable) to indicate where to view or edit lessons, courses, pathways.

But that’s pretty basic. I basically think about my paths as much as I think about my variable/function/class names. I hope that helps.

2 Likes

Think about Google Maps: the URL contains coordinates, map type, searched text, start and end direction locations, etc. Anything you can provide to change the state of the map you are looking at, is reflected in the URL.

Or think of an inventory app built in Anvil: the main page will have 2-3 dropdowns to select some categories and subcategories, a search text box, custom sorting and other page state attributes. Every time the user types a character in the search box, changes a drop down or does anything in the page, the URL changes accordingly. Most changes will not change the browser history, some will. At any point in time you can click on the address bar, copy the URL and you are sure that the app opens in the same state: same filters, same order, etc.

My rule of the thumb is that the browser history only changes when the form changes or in some rare cases when the form remains the same, but its content radically changes. For example if I’m looking at one inventory item and click on the next item button: the form will redirect to itself with another part id, adding one step to the browser history. When something minor changes, like the filtering or sorting, the URL is updated without real redirection.

An inventory page like this starts with 3-4 values in the URL, then, as the app becomes more complex, the number of values increases to dozens. Rarely there are verbs or anything meaningful. The URL starts with the name of a form/page, yeah, that does mean something, and continues with a jumbled list of values.

This works for all my apps and for the big boys like Google Maps, Amazon, etc.
I never felt the need to assign any structure to the URL.

3 Likes

Thank you for a most helpful reply. I knew there were architecture-level tradeoffs beyond the trivial ones. But because they’re non-trivial, one cannot easily derive a listing from first principles. A listing, and introduction to each, really helps.

I’m sure there are more tradeoffs out there, yet to be directly discussed.

I never considered providing multiple views of the same datum or data structure at the same time.

Although, a navigation outline could be considered such a view, for an extended, multi-part data structure such as an insurance product or contract.

Could you clarify that phrase, please?

Now you’re talking architecture! Great! This is exactly the kind of “why” I was hoping to expose here.

The world I come from, desktop app development, has nothing like routing. It is purely an SPA architecture. So I am trying to understand the more general architecture, before attempting to construct any spaces that use that architecture. This includes finding or developing whatever vocabulary is needed to talk about it cogently.

It is evident to me that each set of Routes is created for purpose. Each Route serves a purpose (else why build it?), just as a library function does. It give the user something to see, to do, or both. It has a specific meaning or utility. Its path must be mappable to that meaning. This gives the path itself a meaning. As we do with functions, it should probably be named after what it returns (noun) or what it does (verb).

If it has more than one part, then grammar (word order) comes into play. Order is significant, unless we explicitly code a notation and mechanism to do otherwise.

In short, I strongly suspect that you have all invented your own Routing grammars, one per app, perhaps without realizing it. Your computer program has to be able to interpret your URLs, and tell them apart. This makes the structure formal, in your code, if not in your own minds.

One reason for paying attention to grammar is the parallel with REST APIs. These, too, give access to data and to function. They are for non-human use, but otherwise serve many of the same purposes as a web page. Some app designs even layer one upon the other.

I have heard tales of unstructured APIs, evolving very badly, and did not want to follow the same pitfalls in my URL designs. Having a solid, stable grammar seems key to doing that, at least for paths longer than one part.

1 Like

Since you liked my rule of thumb, here’s another one: the same form often has (at least) two paths:

  • One with just the functionally required info.
  • One with all the form attributes.

The first is typically used in notification emails. The second is for when you want to copy/paste the URL and return to the exact same state.

This is similar to a function with a few required arguments and several optional ones, except in this case, if you include one optional argument, you usually need to include them all.

The form checks for the optional arguments in the URL and uses them if they’re present.
If they aren’t, it looks for their values in local storage.
If they’re not there either, it falls back to default values.

In summary:

  • Some form state must be in the URL (e.g. you can’t show an inventory item without knowing which one).
  • Some can be in the URL (e.g. options like show availability, show price, etc.).
  • Optional values can be stored in local storage and used as a fallback if missing from the URL.

This covers both:

  • Notifications, where the URL only includes the required values (e.g. “show this item for this client”), and
  • Full URLs that include every available option for restoring full state.

That’s a very good point: paths to specific app states are similar to REST endpoints.

But I see two key differences:

  • REST is meant to return data and doesn’t care how it’s presented, whereas a UI may need extra UI related arguments.
  • REST paths are used by developers who appreciate a clear verb/noun design, while UI URLs are mostly copy/pasted by users who rarely look at them.

In my experience, simple REST APIs with only a few arguments tend to include everything in the URL. But once complexity increases, input is moved to the payload.

That’s what I see in Microsoft Graph APIs to interact with SharePoint:
GET requests put everything in the URL (some in the path, some in the query string),
while POST requests accept more data in the payload. The SharePoint web app URL on the other hand is super messy.

My Anvil apps follow a similar pattern: clean URL design for REST APIs, and a super messy one for the app UI.

1 Like

Sorry @p.colbert , multi-view is poor term. What I was trying to convey is the case where you have substantially different display states to the user. Calling them pages seemed a bit confusing considering the conversation. If using routing these would be the defined “routes”. In the typical anvil SPA… it feels like there is not a clean distinction. But generally, each time you are using some form of navigation.

I can see how coming from developing desktop applications routing would seem sort of odd. I can’t think of the benefits to make Photoshop as a anvil app with routing. Really Google Maps doesn’t need it outside of the shareability of a map state. In these cases, the data structure is static and so is the view.

With routing, I almost think about each of my routes as a unique app. Each route has specific responsibilities, data and authentication. For my use case, this feels natural.

3 Likes

Ah, another design decision: what a path actually means (or specifies). In this case, a path encodes or specifies the state of the App instance running in someone’s browser (possibly including “extra UI related arguments”).

A path might also encode

  • The identity of a user, a role they play, a data object.
  • A function to be performed, possibly with parameters.
  • A specific step, in a multi-step workflow.
  • A specific subpart of a structured data object.
  • An entire sub-application.
  • Some comprehensible combination of the above.

Or something we haven’t even thought of, yet.

I might also guess that you could design a path to require specific permissions, or have a limited lifetime. So the world of paths is semantically very rich, or at least potentially so.

What you’re describing is the behavior of a form and the attributes that affect it, not the path itself.

When the form is updated and new UI elements are introduced, the path will include the corresponding attributes. These changes simply reflect the UI structure.

The real design effort belongs to the form. Its functionality, usability, and consistency are what matter. The path is just a container that mirrors those decisions, and in my world (or Google’s or Amazon’s) it doesn’t warrant special attention.

1 Like

I think another important point is: a lot of the state information that gets stored in the URL gets stored as query parameters, which means you don’t have to worry about your users wanting/trying to reconstruct them. The only people that do that are some technical people (or maybe it’s just me – and I fail frequently :laughing: )

So just make the paths that make sense as if you were just naming functions and variables – if you wanna change it later, just like those, you can do it with just a flick of the keyboard.

1 Like

Thanks, all!

I think I’m starting to get a taste of some of the many design patterns available.

Some of you have chosen design patterns that minimize certain kinds of effort, at the start, or during evolution of their App, according to the features and tradeoffs that fit their App (or App Suite).

Other application architects will find other design patterns, fitting for their Apps. There’s no one size fits all, nor should there be.

Some patterns emphasize flexibility for the developer, at the cost of unstable URLs. Users who want to bookmark a particular “page”, because they use it frequently, may then be out of luck. If the user is a paying customer, then I’d definitely want to factor that in, and tweak my design accordingly.

Thanks for giving me so much food for thought!

1 Like