dukai.net
2017-09-08 · 10 min

Decision making process for isomorphic web applications

Why minimalist, non-opinionated frameworks outlast the framework-of-the-month - a guide for developers and architects.


Why

My design and selection considerations come from years of experience with large, complex, UI-heavy applications and industry best practices. The collection of ideas below explains why specific decisions made sense at the time, while highlighting all considerations regarding software architecture. This is a guide to help developers and architects understand specific choices and preferences. It also introduces the importance of proper naming conventions as an organizing principle.

Expectations & requirements

  • The product replaces an existing legacy system with a high level of complexity (many features) and different configurations (product-level offerings).
  • It will have a long lifespan; maintainability is paramount.
  • Multiple large teams will work on the product, so heavy parallel development is expected.
  • The new software is mobile-first (responsive UI), so high performance and quick response times are paramount.
  • Test coverage should be maximal and include both visual and non-visual components.

General considerations and frameworks

Non-biased, non-opinionated minimalist framework (mostly vanilla, with naming conventions)

Frameworks usually break into multiple distinct groups:

Opinionated (biased) frameworks bake design patterns into the code, forcing developers to follow them. Generally, design patterns are good - they ensure cohesion and let less experienced developers follow them. But the moment a real-life scenario no longer fits the predefined pattern, developers start to struggle against the framework, which delays implementation, degrades code quality (dirty hacking), and impairs the final product. Strongly opinionated frameworks are favoured by teams whose final products do not need to be high quality (small business contract work, generic websites) or where a small number of senior developers must deal with a large number of inexperienced or junior developers (sweatshop pattern). Typical examples: Ruby on Rails, AngularJS.

Non-opinionated frameworks help with specific problem areas without enforcing internal application structure. They might offer useful design patterns but never enforce them. Developers can build fast where the pattern matches requirements and switch away cleanly when an exception emerges. My experience: about 60% of an application follows a single pattern, and the rest breaks it one way or another, while still representing valid real-life requirements. This style of development relies on good architecture decisions, developer knowledge, and discipline. Typical examples: Hapi.js, Dust.js, Gulp.js.

Minimalist or vanilla implementations minimize the knowledge needed to join a project by keeping the framework lightweight and relying on examples and naming conventions instead of obscure APIs to organize structure. Humans are very good at pattern recognition but have a limited capacity to memorize large APIs. Generally, the best architectural decision is to keep everything as simple as possible (even when it is hard to resist!). Minimalism does not mean banishing third-party libraries; it just resists invalid justifications - something used because it’s “cool”, or for an insignificant shorthand. This approach takes readability and maintenance as the primary focus. (An application’s lifespan is less than 3–5% writing it and more than 95% maintaining it.)

Isomorphic web applications

Both sides created with JavaScript: server side using Hapi.js with the non-intrusive view engine Dust.js; client side using vanilla JS, Bootstrap, and the same Dust view engine.

In a classic web application architecture, the server side was implemented in Ruby, Java, .NET, PHP, etc. - while the front end was always HTML, CSS, JavaScript. This division forced developers to choose between server-heavy or client-heavy solutions. Server-heavy traditionally meant low front-end quality and comfort. Client-heavy meant slow downloads, parsing, and DOM manipulation. A balanced solution always meant doubling the implementation and maintaining business logic on both sides in two languages. Since Node.js, isomorphic web applications became possible - same language on both sides, large portions of code shared. This lets developers optimize execution to whichever side is most efficient, without code duplication. Hiring developers who don’t need to know multiple languages is also easier. For these reasons, isomorphic web applications are vastly superior to any classic type.

Non-intrusive view engine

View engines merge application data with HTML markup to generate dynamic UI. Many varieties exist with very different syntax, often reflecting their creators’ philosophy and knowledge of the markup language. Unfortunately, many treat markup as a second-class citizen and distort or augment the code. This intrudes into the UX developers’ domain, who then have to deal with the obfuscation on their side. Such overlap can be justified in tiny full-stack shops; most of the time, it falls under the same minimalism principles I mentioned earlier and is just as malicious as overusing libraries based on a coolness factor.

Dual rendering

For higher performance, views can be executed on both server and client side. Initial rendering happens on the server; any additional DOM manipulation (partial rendering) occurs on the client. (Google performance conference video.)

Dual rendering provides significant performance gains and lets us target any resource with a direct, permanent URL (SEO), e.g. a list of contacts or a single contact’s detail.

View models

View models contain view-related logic (transformation, rendering, configuration options) and are 100% reused on both sides.

View engine syntax does not intrude or distort the HTML domain

Minimal overlap gives both UX designers and developers more freedom. Views are reused on both server and client side. Intrusive view-engine frameworks inject instructions into the DOM (AngularJS, Knockout) and store rendering and binding information in special HTML tags. Tags often hold JavaScript code, which opens the door to embedding complex conditions in markup. This is bad practice - part of the business logic separates from the rest, making debugging and maintenance much harder.

Server-side unit tests for all models and every other piece of business logic (100% coverage)

Use Lab.js, Mocha, or Chai. Testing is a must on both sides. Server-side code is generally simpler to test than client-side, where logic morphs together with UI behaviour (Cucumber-style frameworks mimic user interaction and never really isolate a single piece of logic). With isomorphic apps, view models containing the majority of transformation logic can be exercised in simple server-side unit tests.

UI testing with test harnesses

Isolate individual elements to decrease testing complexity. Test harnesses also help UX designers and developers work with smaller components, and help QA test simplified, isolated use cases.

UX developers usually create HTML mocks based on UED designs. With an intruder-style view engine, those mocks get taken apart and distorted by developers. This makes both sides work harder. Dust.js does not distort HTML - mocks can be used directly. We can provide visual test harnesses for each visual component to display and manipulate partial UI elements. This eliminates the two-phase UI design split. Stakeholders and concept designers can review mocks much faster, and developers can add their logic without breaking the mock. From that moment, real data injection and edge cases can be studied without external dependencies, increasing iteration and validation speed enormously while keeping JavaScript and HTML cleanly separate.

Full validation with Joi (comes with Hapi)

A great way to simplify server-side code is to validate and default as many input variables as possible. Then deeper code can avoid scattered parameter checks. Joi provides exactly that. Validation schemas aren’t tied to their endpoints - central storage is possible, and the same schemas serve unit tests too.

Module separation based on naming conventions, single build process with Gulp

For performance, we combine all the resources we download to the client: CSS, JavaScript, client-side views. Many tools exist (webpack, etc.). Many of them again obscure the process, making special rules and exceptions harder. Gulp follows the non-opinionated, minimalistic approach, letting us perfectly adapt to our own needs (naming conventions for plugins, fine control over asset management).


- Tamara Dukai