Moving from express to fastify, pt 1
Tom MacWrighton
The JavaScript ecosystem has changed a lot in the last 15 years.We’ve gone from CommonJS to ESM, from browserify to webpack to vite,from mocha to vitest, from the request module to using web-standardfetch everywhere. The constant reinvention canbe exciting or annoying from one day to the next.
Amid all this churn, express has persevered.Express is a web framework for Node.js, introducedby TJ Holowaychuk in 2009. Like many early JavaScriptmodules and some of TJ’s projects, it took inspirationfrom the land of Ruby - in particular, Sinatra, whichwas established in 2007.
// This is a tiny example of using
// express. Notice how there's almost no boilerplate
// involved in creating a tiny web server.
const app = require('express')();
app.get('/', (req, res) => {
res.send('Hello World!');
Express is a beautifully light abstraction: a‘hello world’ example is barely over 10 lines. Compared to the boilerplaterequired to do the same in Ruby on Rails or a Java server at the time, itwas a breath of fresh air.
Express became the default for new JavaScript projects and has stayedthere. In a typical week, express is downloaded from NPM 30 milliontimes. It’s a roaring success in every way.
Express is an expressive framework: like a classic Ruby orJavaScript framework, it lets you color outside the lines usingloosely defined objects and types. Query string parameters in expressbecome freeform objects.The same goes for the bodies of requests, and the kinds of responsesyou can send: it’s all a bit ad-hoc by default.
But the vibes have shifted: we write TypeScript now,and everyone’s excited about validation modules like Zodand TypeBox. Interfacesare less ad-hoc and more declared, tested, and strictly typed.
Modern tooling is written to take full advantage of typesand schemas. With tRPC, we’re writing backendendpoints and calling them from auto-generated, auto-typed functionson the frontend. Other backends are embracing OpenAPI(née Swagger) to generate clients, test suites, and more.End-to-end type-safety is in vogue, and it is good.
In our case, we want our types as an OpenAPIspecification.OpenAPI is a JSON/YAML document that lets you describe your entireREST API strictly: what URLs are supported, how parameters, headers,and request bodies should be formatted, what kind of responseswill be produced. It lets you enrich this strict machine-readableinformation with human-readable descriptions and helpful examples.It’s a pretty exciting time for the OpenAPI spec.
A number of companies and open source projects, includingStainless, Speakeasy,and OpenAPI TypeScript, arewriting tools that can generate functional, clean SDKs straightfrom OpenAPI specifications.
You can also generate great documentation from OpenAPI specifications.We’re already generating api.val.town/documentationusing a library from Scalar.
And in the near future, we should be able to use our OpenAPI specificationto give our AI assistantthe ability to interact with our API via function calling:conveniently, OpenAPI and the OpenAI function callingsystem are both based on JSON Schema,so translating between the two is possible.
Val Town started off with Express. It has served us pretty well.But the papercuts started to accumulate.
When we started creating a public API with an OpenAPI spec,we immediately started to get a bad feeling about using expressas we were doing before. The OpenAPI specification was makingstrong promises about the kinds of requests we accepted and responseswe’d return. But the specification was just a dead document,a YAML file - it wasn’t part our codebase, or our test suite. So itwasn’t enforced or validated, and sure enough, it was slightly incorrectfrom day one.
The other pain point was async. We write the Val Town backendas TypeScript but try to transpile it to modern code, sowe keep our async functions and ESM syntax when werun our code. Express far predates the existence of theasync & await keywords in JavaScript, and it does not mix well with them.
Because express is incredibly popular, there are ways to implementOpenAPI with it, and support async handlers - plugins likeexpress-openapiand express-async-errors. But these are a bit bolted-on:they make it technically possible to achieve the goal, butnothing about them is idiomatic.
Another option would be waiting for the next version of express, v5,which includes support for async functions. That release istaking a little while to complete, so we didn’t wantto bet on it landing soon.
It is great, though, that expressis still actively maintained, and I don’t envy the task of rollinga major update to such a heavily-adopted project. There are certainlycodebases with tens of thousands of lines of code that rely onexpress and can’t easily jump to an alternative.
But Val Town’s backend is not an enormous legacy project.Switching web frameworks wasn’t going to requirea deadly from-scratch rewrite. We were ready tobid adieu to express.
Technology choice is complicated. I don’t want to make a decisionbased on some simplistic criteria like GitHub Stars, but it’s equallyfraught to rely on vibes and instinct. So I investigated whetherprojects had good documentation and maintainership, if they hada decent history of stability, and perhaps most importantly, whetherthey were trying to solve the problems that we needed to solvhttps://blog.val.town/_astro/fastify-logo.9OHOW880_Z2hu2dU.webp9OHOW880_Z2hu2dU.webp" alt="Fastify" loading="lazy" decoding="async">
We landed on fastify as the technology to build thefuture of Val Town’s public API. Some of the strongest points forfastify were:
- A comprehensive ecosystem,which includes a lot of high-quality, first-party plugins. We’rebuilding a system in production, which means things like ratelimiting are going to be necessary, and it’s amazing thatfastify’s rate limiteris well-architected.Great, thorough documentation,and an active community that has been really helpful for support.Solid support for plugins and encapsulation that let us structure the application into sub-applications.Support for validation and serialization with multiple validators and validation for all inputs and ouputs.Good support for async/await, like you might expect from a modern framework.Off-the-shelf integrations for opentelemetry and Sentry meanthat we could have tracing and telemetry from day one.We were already using the light-my-request module to testour express server. light-my-request is written and maintainedby the fastify folks, and it is built into fastify.Finally, @fastify/expresslet us incrementally migrate from express to fastify.
Of course, fastify is also very fast, but web framework overheadwasn’t a problem with express and most likely won’t be a problem withany framework we choose: most of our performance envelope has todo with operating the Val Town runtime and interactingwith the database.
We considered a few other options, like Hono and Elysia.Both are lovely projects - we use and recommend Hono a tonin Vals. Elysia has convenient typesafety and a lot of features,but its support for Node.js is very experimental. And Hono hasa first-party OpenAPI plugin, but some of the ecosystem around fastify - for tracing,incremental migration from express, and plugins - was more built-out.
For simpler projects and especially for situations where we wantmulti-platform compatibility so that code can run on Node.js, Deno,or Cloudflare Workers, we reach for Hono.
By using @fastify/expresswe were able to do this migration entirely incrementally: the firstfastify PR just added the fastify dependency and wrapped our expressserver with fastify. Then each successive PR moved a few routesfrom express to fastify, until the application has beencompletely ported.
As of today, we’ve ported all but two routes in our backend server.It’s been an incredibly incremental effort, thanks to @fastify/express -such that we never had to stop work on feature development or freezepart of the codebase while switching frameworks.
And the best is yet to come – next we’ll be talking about ournew TypeScript SDK, which is generated directly from the OpenAPIdocuments that fastify produces, as well as improved documentationand more robust API endpoints. Stay tuned!
Edit this page