So you want to make a web app
December 13, 2024
Let’s say you want to make some software. You want your friends and loved ones, and maybe even some strangers, to log into something and accomplish something useful or fun. You’d love for it to live on the web, because who downloads apps anymore, plus somewhere deep down you’re still an idealist who wants the web to win. The situation is this: it’s 2024, and you’ve made the regrettable decision to make a web app.
If you’ve had a job in software development at any point in the last 10 years, odds are pretty good that you know React, and maybe even use it regularly. So it sure seems like a good choice if you want to make quick progress. You’ll just have to ignore all the noise about React being obviously the wrong choice for most use cases, and forget the countless times you or your colleagues have said, “Holy shit React is terrible” to each other, and forgive React’s maintainers for changing its APIs and best practices drastically every 1-2 years, and tune out all those happy people using Svelte, not to mention all the sickos using HTMX, and—OK, stop, just use React.
Or, actually, just use NextJS, which is a full-stack framework
recommended by the React team themselves.
NextJS appears to be steadily consuming React and slowly building itself into a
fragmented, buggy, VC-bloated version of Rails. Fine, whatever. To be fair, when
it works, it is truly neat—you get to write the React code you’re comfortable
with, regardless of whether that code will execute at build time on your
machine, at request time on a web server, or at runtime in someone’s browser.
Though in reality you do have to pay some mind to where your code will
execute: if it’ll be run at build time, you don’t have access to any request
state; if it’ll be run on the server, you can’t use basically any React features
and might as well just be writing plain HTML; and if it’ll be run in the
browser, you need to make sure to flag it to the NextJS compiler or your app
won’t compile or run at all. And it’s not always obvious where your code will
run or what server/client resources it’ll have access to, so it takes some trial
and error to get everything up and running consistently. But, if you can stay on
the happy path, a lot of what NextJS allows you to do is legitimately great, and
a lot of the concerns it conceals from you would be a pain to deal with on your
own, so it’s probably worth the trade. Plus, a handy little boilerplate
generator is only ever an invocation of npx create-next-app@latest
away, so
who are you to complain?
That is, unless you don’t want to use npm to manage your JavaScript packages. Maybe you prefer Yarn, which is the same thing as npm except it used to be more popular than npm and some of its docs claim that it’s faster for some use cases. Or maybe you want to try pnpm, which is the same thing as npm except it it’s faster and more “disk space efficient” (should that be something to worry about for a brand new side project?), and if you check its site you’ll also find this foreboding section in its FAQ. Or maybe you want to try Bun, which is actually an alternative to NodeJS altogether (much faster, of course) that also happens to replace all npm functionality while chucking in a bunch of other features. Or maybe you want to try Deno, which appears to be the same thing as Bun, except that it’s made by the guy who ruined your life by inventing NodeJS in the first place, and it claims to “uncomplicate JavaScript,” and you sort of have to wonder if the folks over at Deno HQ know how absurd it is to use the word “uncomplicate” at the top of a page that lists a bunch of stuff Deno does that’s different from the JavaScript you’re used to, tries to sell you enterprise support and two different (!) paid cloud hosting solutions, advertises their own NextJS alternative (which, by the way, uses Preact, which is the same thing as React except that it has a P at the front), and after all of that still includes a “Why Deno?” link at the bottom.
So npm it is, then? Great, works for me.
By now you’re probably itching to start writing some code. Dream on, chump! Unless you only want a toy that can never graduate past http://localhost:3000, you need to spend a few days solving the interconnected problems of how you’ll authenticate your users, where and how you’ll store their data, and where your code will be deployed. These may sound like the kinds of concerns you could easily fold into an in-progress project later, but the beauty of the modern web dev landscape is that each one of these is going to impose its own constraints on what your app can do and how you’re allowed to implement it.
Authentication will require you either to roll your own password auth, find an open source OAuth library, or make an account with an all-in-one auth provider and hope you never exceed their free usage limits. Whichever you choose, you’ll then also need to make some changes to your NextJS middleware and server routes to verify the presence of valid auth tokens on every request (usually by copy/pasting almost-functional code from somewhere on the web). If you want to store any information about your users in your own database, then you’ll also need to wire up some sort of integration between that auth system and your DB, which might entail a convoluted integration between two or more libraries or might require you to (I’m not joking) deploy a completely separate app to receive webhooks from the auth provider. The simplest starting point for all of this is Auth.js, an open source library that lets you plug in arbitrary auth methods and storage systems, and mostly works in spite of being a beta release.
Here’s where things start to get a little funky: if you’ll be deploying your app
to one of the more popular hosting platforms (more on these later), then a lot
of your code will probably be running in something called an “edge runtime,”
which is a JavaScript runtime that is neither a browser nor NodeJS (nor Bun, nor
Deno) and comes with a bunch of arcane
tradeoffs. Among other annoyances, this means you’ll need to connect to your
database without using TCP, which means you’ll need to use a database driver
that can communicate over HTTP. Such drivers are known as “serverless” drivers,
because nothing screams “serverless” quite like updating your application code
to accommodate the limitations of the deployment environment, am I right? Oh and
by the way, if you still want to be able to run your app locally with next dev
after switching to a serverless driver, you’ll also need to run a proxy in front
of your local/test DB instance that can support these newfangled HTTP
connections—congratulations, you’re now running a local server to emulate your
serverless database and prop up your serverless application.
The above paragraph is really just a preamble to choosing a database, which we now know needs to be edge-compatible. If you want a NextJS-friendly relational DB these days, the SEO overlords and YouTube nerds with LEDs on their walls will push you towards one of a few options, all of which have free tiers and nearly identical marketing materials: PlanetScale (MySQL), Neon (Postgres), Turso (SQLite), Cloudflare D1 (SQLite), or Vercel Postgres, which is, I shit you not, literally just Neon but it costs more money and has fewer features. If you are one of the world’s many Certified Postgres Enjoyers, Neon is your best option, and it’s actually quite good (it works! the docs are well-written! it’s fast!), if you can squint through the sheen of their trendy dark-mode admin console. Now all you need to do is decide how you want your code to interact with this database to execute queries. So you’ll need to decide if you’d prefer to use a TypeScript ORM or to rawdog some SQL strings over the wire. People say that ORMs make writing type-safe code easier, and if you use one you won’t have to worry as much about injection attacks, so you probably want to start with one of those. Here’s the thing, though: all ORMs are bad and require you to ignore the vast majority of their features to be productive. So whether you use Prisma (older, stable, resource-oriented API and a horrendous custom DSL for defining schemas) or Drizzle (newer, pre-1.0, more SQL-like API, nice to use but lots of missing features) you’re going to spend half your time hammering out queries and half your time wanting to bludgeon yourself with a hammer. Either way, you’ll write a bunch of pseudo-SQL-by-way-of-TypeScript, find fun ways to jam raw SQL through the API’s many gaps, and try your best not to touch anything once it finally works.
With a couple more config files at the root of your repo and some honest-to-goodness persistent state, it’s just about time to figure out how to deploy this thing. Vercel might seem to be the obvious choice given that they own and maintain NextJS, but by now you’ve likely spent enough time in their docs to resent them deeply, so you’d be forgiven for looking elsewhere. Cloudflare is a pretty widely loved CDN that’s getting into the public cloud game, and they’ve got a frankly bonkers free tier. Why don’t you give them a shot and see how it goes?
Cloudflare has two different products that can host NextJS apps: Next on Workers, and Next on Pages. The difference between them is extremely uninteresting and incredibly obtuse. They both claim to be serverless environments for hosting your app, but each one supports a different subset of JavaScript/NextJS features, so once again you’ll need to tailor your code to whichever not-server it’s going to run on. Of the two environments, Pages has fewer practical tradeoffs at this moment, so it’s probably the better starting point if you ever want to release anything. Here’s what you can expect: hosting that really does work quite well at the cost of a truly taxing development experience. The Cloudflare UI will be impossible to navigate, you’ll need to copy all your env vars to yet another file, your application logs will be hard or impossible to find, and you’ll need to emulate the Pages serverless environment locally in order to find out if you app will actually run or not (and when you do, you won’t have access to your sourcemaps, so debugging any complex issue will require deploying to their infrastructure anyways). But once it’s up and running, Cloudflare’s global network of compute/storage nodes with various JS runtimes really will do their jobs and run your NextJS app—that is, as long as you only use these features, and can get comfy with uncached page load times being slow, and are extremely careful never to share any in-memory resources across requests (“requests,” by the way, being exactly the type of useful concept that NextJS provides no real visibility into).
Assuming you can survive this gauntlet of meaningless decision-making and not-at-all-transferrable knowledge-gathering, you’ll earn the privilege of writing some actual application code! You can now look forward to wading through the ecosystem of CSS libraries, pre-built component libraries, data validation libraries, and the good old React library to cobble together your very own masterpiece. If you even still want to anymore.