The future of legacy web applications

Diego Santana
Flatiron Engineering
6 min readNov 18, 2022

TLDR: In recent years, new ideas in web development frameworks have made it easy to build simple web applications. But improvements to complex legacy web applications have stagnated. Flatiron, like many mature software companies, is looking for a framework that makes it easy to migrate our dynamic web applications to a modern auto-scaling environment.

Why Flatiron cares about web performance

Flatiron Health cares about the Internet. Our product, OncoEMR, was one of the first electronic medical records to be hosted in the cloud. Every day, doctors around the country use OncoEMR to provide the best quality of care to patients with cancer. When the performance of our applications suffers, patient care does too. We take performance seriously.

What’s been happening in web performance

Over the past ten years, there was an explosion of technologies and frameworks for building web applications. Every part of a web application — frontend user interface, backend API server, database, and infrastructure — saw rapid innovation. The new tools were fun and exciting, but they also led to a sense of instability in the industry. The industry fragmented. Developers had to make a lot of decisions to build and maintain their web applications.

There is a movement now to simplify the complexity that comes with building web applications. A huge change was the rise of server-side JavaScript. JavaScript, which was originally only used in the browser, became a viable option for writing server-side applications. Engineers can share common code across the front end and server if both are written in JavaScript. For engineers who prefer strongly typed programming languages, TypeScript adds type-checking to JavaScript.

As JavaScript-based frameworks became a bigger part of the industry, a company named Vercel has arguably become the most successful innovator and simplifier. Vercel’s NextJS uses JavaScript (or TypeScript if preferred). The framework blurs the line between frontend, backend, and infrastructure code. NextJS provides an opinionated developer experience for creating highly-performant web applications. Developers don’t need to worry about which tools to use within NextJS’s boundaries. A lot of the complexity is abstracted.

Framework gaps

At Flatiron, we carefully evaluate new tools to see if they can give us benefits over our current ones. And after studying popular JavaScript frameworks, we see limitations that make us hesitant.

Flatiron is a mature software company. OncoEMR is a complex web application that has been in active development by multiple teams for well over a decade. The application is built to enable all the necessary workflows to run the daily operations of an oncology clinic: billing insurance, scheduling appointments, prescribing drugs, administering infusion therapy, writing notes, etc.

Flatiron has also built smaller, simpler applications over the years for both internal and external users. We’ve created apps for tasks like updating configurations and hosting documentation.

The JavaScript frameworks that we evaluated make it incredibly easy to build simple applications. We can use these frameworks for that purpose with no reservations. But migrating even the smallest workflows of OncoEMR to a new framework is a risk that may not be palatable.

Currently, the OncoEMR server is written in .NET. .NET is a fantastic framework for developing complex web applications. Microsoft owns .NET and has molded the framework into one of the most popular in the world. However, .NET is falling behind the latest trends of web development and performance.

Microsoft may be missing a prime business opportunity here because in addition to .NET, they also own TypeScript and a cloud computing platform, Azure. As Vercel and others created frameworks to unite and simplify many web development ideas, Microsoft has not.

Flatiron, like many mature software companies, is looking for tools that can bridge the gap between our current technologies and new ideas about web development. There are three big features of a framework that we are searching for:

  • Shareable code across front end and server
  • Easy migration from an application’s existing state
  • The ability to be hosted in an auto-scaling environment to handle varying user traffic

That harmony between modern web performance and complex web applications is achievable. Microsoft, for one, is in a prime position to accomplish that goal.

Currently, .NET has an internal framework called ASP.NET for building dynamic web applications. It’s great already and could be expanded to incorporate ideas from modern JavaScript frameworks.

Let’s call this expansion Web.NET.

This version of Web.NET takes cues from NextJS, .NET Core, and plenty of others. Hopefully it feels coherent enough to be useful.

Here’s the user interface, written in TypeScript.

import { useLoader, LoaderFn, useAction, ActionFn } from 'deps.ts';
import 'Utils.Models';
import 'DataAccess.Posts';

export const Loader: LoaderFn = ({ context }) => {
return {
posts: async () => {
const repo = context.GetService<PostRepository>();
return await repo.GetTopPosts();
}
};
}

export const Action: ActionFn = ({ action }) => {
return {
deletePost: async () => {
const postId = await action.get('id');
return await deletePost(postId);
}
}
}

export function Root() {
const {data: { posts }} = useLoader<Post[]>(); // alternatively: const {posts} = useLoader('posts');
const {actions: { deletePost }} = useAction<PostActions>();
return (
<ul>
{posts.map(p => (
<li key={p.id} onClick={deletePost({ id: p.id })}>
{p.title}
</li>
))}
</ul>
);
}

The Loader function describes what data is needed for the associated React component. Inside the React component data from Loader is accessible via a framework-provided useLoader hook. Complex web applications rely on multiple pieces of data. Loader supports that by allowing the developer to return an object containing different pieces of data.

The Action function defines different actions that the user can perform via the component. Like Loader, the Action function supports multiple actions by allowing the developer to return an object.

The Loader and Actions functions can be run on a server, on the client, or in a serverless Azure environment. The React component could be rendered on the server, or bundled by Webpack (or any other bundler) and shipped to the browser. Client-server communication is handled automatically. Web.NET could use REST, GraphQL, or gRPC. All these choices can be made by the framework, not the developer.

The user interface code is importing namespaces from other .NET projects written in C#. In Web.NET, TypeScript is another interoperable .NET language. Currently in .NET, all code that is written in one language is usable in any other .NET language. That means C# can import F#, TypeScript can import C#, etc. All combinations are easy.

Here, Web.NET is importing one namespace that allows us to use C# data models and another namespace where we can retrieve data from the database.

Another implication is that TypeScript has access to the whole range of Nuget packages for .NET. Conversely, .NET has access to any third-party TypeScript library.

That means Web.NET could handle React in an existing .NET language. JSX syntax used to write React components is syntactic sugar for the React.createElement function. Web.NET would allow calling that function from C# or F#.

using WebNet;
using React;
using Posts.Model;
using DataAccess.Posts;

namespace Root
{
public class Component : BaseComponent
{
public async Task<WebNetLoaderData<IEnumerable<Post>>> Loader(LoaderContext context)
{
var repo = context.GetService<PostsRepository>();
var posts = await repo.GetTopPostsAsync();
return new WebNetLoaderData(posts);
}

public ReactNode Render(ReactProps props)
{
var posts = WebNet.useLoaderData();
var postElements = posts.Select(post => React.createElement("li", new { key = post.id }, new ReactNode[] { post.title }));
var listElement = React.createElement("ul", null, postElements);
return listElement;
}
}
}
// F#
open WebNet
open React
open Utils.Models
open DataAccess.Repository

let loader (context: LoaderContext) =
async {
let repo = context.GetService<PostsRepository>()
let! posts = repo.GetTopPostsAsync()
{ data: posts }
}

let render props =
let e = React.createElement
let createPostElement post = e("li", { key: post.id }, [| post.title |])
let posts = useLoaderData() |> Seq.map createPostElement
e("ul", null, posts)

module Root
let component context =
match context with
| LoaderContext -> Some loader
| RenderContext -> Some render
| ActionContext -> None

The developer can write a React user interface in any .NET language, follow framework patterns for describing what data is displayed where, and the framework handles the rest. If the code is hosted on GitHub, GitHub Actions could ensure that the updated web application will be live in Azure within minutes.

Web.NET would be valuable to Flatiron. We already have tons of .NET code ready to be reused. TypeScript as a .NET language would be valuable to Microsoft and the greater software development community. There would be huge complications to work out. But by combining the worlds of .NET and JavaScript, people could collaborate like never before.

Web.NET is a draft of an idea. It’s not perfect, but the conversation is worthwhile. Can Microsoft embrace this new era of the Internet? Maybe. Hopefully. Competition and collaboration are accelerators for progress.

At Flatiron, we work hard to enable engineers to write performant and user-friendly web applications. Oncologists depend on the performance of those applications to provide high-quality patient care. .NET has served us well through the previous decade of the internet. And as the industry shifts, there’s great promise that Microsoft can be a voice that pushes the Internet forward.

--

--