One full-stack to rule them all!
Originally posted on Medium
Yes, yet another todo example app! 𤌠Needed to throw my hat in the ring because the existing solutions werenât cutting it.
We want a stack that enables rapid iteration as requirements change without producing bugs. Ideally, it should scale in terms of traffic and developers without requiring costly re-writes.
To achieve this we chose components that are:
- A single language, eliminating developer context switching
- Type-safe, eliminating a whole class of bugs
- Tested at scale in production
- Used by enough developers that solutions are easy to find
Why?
Coming from Google, I realized the importance of static types across the entire stack (including the API). There are thousands of engineers working on Search and Ads at Google (I was one of them). No one person completely understands the systems because theyâve been built over 10 years by thousands of engineers. Bringing data from the database through the Ad Server and onto Search crossed ~8 different services. Making these changes was scary, to say the least.
Static typing across the stack and a single mono repo are what make this possible. Static typing ensures the data is there and of the expected type when you access it. Mono repo ensures all services are in the same state and the API contracts (Protobufs at Google) are enforced by the compiler. At Google code that doesnât compile canât be checked in.
Why TypeScript (TS)?
What other typed languages can run on the client and server and arenât experimental? Having a single language reduces context switching for engineers. It gives us the confidence to own full-stack changes. TypeScript is rapidly growing in popularity. Itâs a superset of JavaScript which is the most popular language of all time so most programmers are at least somewhat familiar with the syntax.
TS eliminates 15% of production bugs. It scales to hundreds, if not thousands, of developers. Microsoft, Google, and countless other companies rely on it in production systems with billions of users. Google is increasingly adopting it over Closure (Googleâs prehistoric typed JS language). It has great documentation and IDE support with a thriving community (3rd most loved language on StackOverflow). It presents clear errors when the compiler fails.
The TypeScript team has been rapidly improving TS across many dimensions. In the last year they added:
- Support for existing tools to meet developers where they are
- TS + Babel for easy integration in the React ecosystem
- TS + ESLint for better linting support (TSLint is too slow)
- Improved error messages. Instead of highlighting an entire interface when thereâs a type issue it jumps to the property thatâs causing the issue.
- More advanced type checking (no overloads necessary in generics). Ex. Promise.all
Why React?
Itâs 2019! MVC should be dead! Long live JS in templates and one-direction data flow!
JS in templates gives us access to a Turing complete language when weâre constructing whatâs displayed. This pushes complexity to the leaves instead of APIs or random custom functions which reduces overall complexity (see Pete Huntâs talk). In Django (I have a python background pre-React), youâd use tags, filters or write your own custom versions when you run into things like formatting date time ranges. Learning an extra template language and writing complicated escape hatches is a waste!
One way data flow ensures thereâs a single source of truth and you donât run into bugs where the same value could be in different states. Google just announced Compose, a React-like framework, in Android. They dive into the issues with two-way data bindings in their talk at IO.
Since Reactâs public launch in 2013, itâs only gotten better and grown in popularity. Many of the worlds most used sites are completely or nearly completely written in React (Twitter, Netflix, Uber, Facebook, and Airbnb). Companies that value developer productivity are wise to adopt because it significantly reduces UI code complexity over more traditional frameworks.
Why GraphQL with Apollo?
GraphQL provides a simple way for clients to request only the data they need and easily move across relationships. Requests can change without server modifications and deploys. Try that with REST or Protobufs!
If we extended our Todo example to have different users we could query for their specific todos. Any additional fields on either user or todo are emitted from the response but could be fetched in different queries without changes to the API
Apollo has client and server support that requires less upfront learning and setup cost than Relay. It has automated TypeScript type generation through graphql-code-generator. Solid documentation and tooling with a thriving community. Ability to batch queries but can defer until needed, avoiding fragment complexity.
The two major distinctions from a devX perspective I see between Relay and Apollo for queries are containers and cursors. Both are required in Relay (opinionated) and optional in Apollo (flexible).
Container queries batch all query fragments (data required by each component in the subtree) into a single container level query. In the simple Todo list case, this is actually a bit tricky. It involves importing the lower level fragments and composing them up the tree. In Apollo, weâd simply request the data at the component level. The tradeoff is in the number of requests, containers are more efficient at the cost of complexity and difficulty debugging issues.
Cursors are a standard way for pagination in Relay. They require the type youâre fetching to wrap additional details.
Todo GraphQL schema for Relay cursor pagination:
If weâre using Apollo offset based pagination (assuming you have a SQL database this is straightforward), then we just need the Todo type and we pass the limit and offset in the query. The downside is that if new items are inserted or deleted in the original set it can return duplicates or skip results. Cursor based pagination fixes this.
This a simple comparison between Relay and Apollo. For Facebook, Relayâs additional complexities make sense for performance and data guarantees. For us, the added complexity didnât make sense but itâs something we could revisit.
Relay TypeScript Todo example for comparison (linked from the official docs)
Why Node?
Our requirement for a single language across client and server limits our server language choices.
Node has some âregretsâ like security, the build system, and package.json but, the ecosystem is unparalleled.
Node is by far the most popular language on GAE and AWS Lambda. Itâs great for rapid development. However, many companies also use Node at scale in production (Instagram, Netflix, Airbnb, and Walmart).
Is there something else that can run TypeScript on the server safely in production? Unfortunately, Deno (from the creator of Node) isnât there yet.
Conclusions
While there is never truly one stack to rule them all, we believe that our set of requirements likely matches many people building web apps today. While all of these pieces have been battle tested individually the sum of their parts creates a relatively novel way of developing web apps that isnât currently well documented.