Adam Zerner

Pursuing the right tech stack for me

Meta: This post is journal entry-like. It's not well thought out, researched or refined.

I'm a programmer and I like to work on my own projects. Sometimes for fun, sometimes for profit, sometimes for something in between. So then, I need a tech stack.

Yes, the most appropriate tech stack will depend on the app β€” SSG would probably be appropriate for a blog or marketing site whereas a SPA would probably be appropriate for a heavily interactive and dynamic app like website β€” but I think that there is also something to be said for sensible defaults. I think a lot of apps actually have pretty similar needs. At least the ones I would pursue. This makes me think that coming up with a sort of default tech stack would be a worthwhile pursuit.1 I could always tweak it for the project at hand.

Programming language

Let's try to start with shorter, quicker discussions and save the longer discussions for later in the post.

This will be a quick one here. I want to use JavaScript.

Err, actually I should have said TypeScript, not JavaScript. That's what the cool kids are using now, right?

In all seriousness, I do like the autocomplete and being able to hover over some code in my text editor and see it's shape. In opening up an old JavaScript project I hadn't touched in some time, I found myself having to console.log everywhere to see the shape of my data. Yuck. I'd much rather hover over stuff.

I guess there's also the benefit of catching bugs earlier. From what I gather, that's the main selling point of type-safe code. I guess that's helpful. Personally, thinking about the time I've spent coding in JavaScript vs the time I've spent coding in TypeScript, I don't really feel like it's something that helps me all that much. I wonder how common that is. I wonder if it is helping me a lot and it's just hard to notice.

One thing I will say is that I think it makes sense to be pragmatic with TypeScript. There are tradeoffs at place with respect to how hard you push the type safety thing.

As an example, think about numbers. You could say that something is a number. But you could also get more specific and say that it is rational number, or an integer, or a positive integer, or a positive even integer, or a positive even integer between 20 and 86. You see? You can get stricter and stricter and stricter. Well, in TypeScript I'm not really aware of a way to do that but I know in Haskell there are a million different ways to get more specific about the type of a number.

Anyway, the point is that there is trade off with types. It is more effort and complexity to get stricter. My opinion is that you should be pragmatic, think about the tradeoffs, and do what makes the most sense. Which implies that any or @ts-expect-error is sometimes the way forward. That might seem obvious, but other people say to make your types as specific as possible. Smart people who I respect. I disagree, but reserve the right to change my mind as I get more experience.

UI framework

Earlier this afternoon I was reading You Don’t Need A UI Framework. Um, yes I do.

Something like Bootstrap. Except not Bootstrap.

I have this "signals of seriousness" theory. Imagine that there's a website that sells widgets. It might be an awesome place to buy widgets, but when it is your first time on the site, how do you know that? Think: first impressions. It takes time to research and investigate to see how good a product actually is, so instead users rely a lot on snap judgements and heuristics. If the site is kinda ugly and obviously not designed by an actual designer, you assume that it's not "serious", and not a good place to buy widgets. A site built with Bootstrap is a pretty good indicator of that. So for my projects, I'd really like something that is a step up.

And I'm willing to pay for it too. A good amount of money. Thousands of dollars. If a UI framework helped me design sites that are a step up, that's worth a lot to me. However, I actually have had a really hard time finding a good one.

There's a lot of random paid ones that just don't look very good. Tailwind UI is the first one I came across that I thought looked really good. I actually bought it, but subsequently asked for a refund. I realized that they don't really give you pre-built components. Take a look at this. You have to kinda dig through their code and create components yourself. I don't want to do all of that. I want to have pre-built components.

I gave up looking after that. Then months later I picked back up. Again I wasn't having success, but eventually I came across Chakra UI and fell in love. It's exactly what I'm looking for. Pretty much. There's tons of components. It looks great. It's not (too) generic looking. The documentation is great. There's an active team and community behind it. So yeah, I think Chakra is the winner. For my projects, I want to use Chakra.

Database + ORM

This is a topic that is a weak point of mine, but I'll give it a stab.

One question is whether to use a relational or document database.2

All things considered, I don't feel strongly, but relational seems like the way to go. And Postgres seems like a good choice for a relational database.

That brings us to the question of ORMs. For that, I am quite happy with Prisma. I've found it to be pretty easy to use, but what I'm most impressed with is the documentation. Wow. Incredible. Some of the best I've ever seen. That makes me really happy, and gives me trust that developer experience will in general be pretty good.

I have heard that the SQL it generates could be problematic at times, but I think that's only in pretty complicated situations, and if you run into it you could always just write raw SQL yourself, so I don't see it as a concern.

I will say, my one gripe with Prisma is that there isn't an equivalent of rails console that you get with Active Record. I used to love being able to type rails console and mess around, especially in sandbox mode.

Oh, there's also the question of GraphQL. I used it for a few months at one job and didn't particularly like it. I found it a little overly complicated and difficult to work with. I like just sending an HTTP request to whatever RESTful endpoint.

Rapid fire

There's a bunch of things that don't require much discussion. Yes, this is a pretty broad interpretation of "tech stack".

Version control

Git. It's what I've always used and what most people use. Even if something else is better I don't expect it to make me notably more productive β€” the benefits are more likely to be marginal β€” so familiarity and popularity win out.

Repository management

GitHub wins out over BitBucket and GitLab. At this point I've actually used all three in different jobs, and GitHub just feels like it provides the best UX. It used to not offer free private repos which was a problem, but now it does, so it's the winner.

Text editor

VSCode. Seems like the best one if you're working in JavaScript. I used to use Atom but ran into issues with it being slow and with it being hard to get jump to definition stuff working.

Code formatting

Prettier. I love being able to just not think about it.

End-to-end testing

Cypress. I'm a believer in using it sparingly because e2e tests are notoriously flaky, but Cypress is the best framework I've used. Previously I used Nightwatch.js and hated it.

Unit testing

Meh, I don't care. Jest, Testing Library, Mocha. They all feel like they're basically the same thing to me.

Analytics

Google Analytics is the most popular, but fuck that. I don't like the privacy issues. I don't like how it hurts page load times. I don't like how it forces you to have a cookies popup.

Then again, I don't feel like paying for analytics for a project that doesn't have at least a moderate amount of revenue yet. Which unfortunately is, err, every project I've ever pursued. I've searched for tools with a nice free tier but haven't been able to find any.

So I think my plan is to just forget about analytics early on β€” you could always look at your database for a more informal approach to analytics β€” and if my project has enough revenue I'd use Plausible. Plausible looks fantastic.

Oh, and they have a great blog post about how server-side analytics basically don't work because of bots. Good to know.

Payment processing

It's been some time since I've investigated this, and I think I'd just go with Stripe. They're the big player, and so presumably pretty reliable. Their docs are good. I like the founders. There's tons of tutorials.

OTOH, it feels a bit maximalist for my purposes. I just need something simple and minimal. In my experience using it you do have to sift through stuff that is excessive and/or not relevant to you in order to accomplish your task. If there was a more minimal tool that is also good and comparably priced, I'd be interested.

I have seen some concerning stories on HN about people getting screwed by Stripe unfairly and not being able to get in touch with customer support. Hopefully those are rare, but it's something to keep an eye out for.

Build tool

Webpack is too slow. Cold starts are often 10+ seconds, and I start my dev server frequently. Maybe because I'm paranoid about HMR not actually working.

Vite is pretty awesome. I'm happy with Vite. Turbopack is intriguing. The idea that you don't even need a build tool in the first place with Deno is doubly intriguing.

Web framework

Ah, finally, the meat of the post.

Client vs server side rendering

When I was first learning to program, I did some server side rendering with both Cake PHP and Rails. Maybe the first year or so. Pretty soon after that I learned Angular. That lead to be building SPAs for the next nine years or so, in Angular, Backbone, Vue and React.

Then we started using Remix at work about three months ago. That (re)opened my eyes to server-side rendering. To my surprise, I really liked it!

For one, I was still able to use React to build views. I never liked templating engines like handlebars or ERB in Rails. OTOH, React components and JSX are both really nice IMO. I think I had previously been conflating that with liking SPAs. Previously, I had a preference for SPAs, and I think that preference was more about the view layer than actually rendering on the client.

Another thing I was surprised to love is doing the data fetching on the server. With SPAs you have to hit the API to request the data, and then use the response to build the page. With SSR, well, you still kinda have to do the same thing, but it happens on the server. Ie. client makes request to server, server hits database to load data, that data is used to construct the HTML, and the HTML is sent back to the client. It's not conceptually that different, but something about it is just much nicer IMO.

Maybe it's the separation of concerns? With JavaScript based SSR frameworks you usually fetch the data in a separate place from the React component. You can still do that in a traditional React SPA though, you just have to use smart components for the data fetching and dumb components for the presentation. That feels messy to me though, and leads to huge amounts of nested components which is a little ugly.

I also like the routing in server-side rendered apps. It's a little less work.

So yeah, I think I like SSR better. That said, I can still definitely see a use case for SPAs. I think they are a better choice for highly interactive and dynamic apps.

Performance vs simplicity

For what follows, there's a lot of tradeoffs regarding performance vs simplicity. I have the feeling that frameworks are leaning too heavily towards performance. Some apps need the performance, but others don't. So I guess it's ok for a framework to optimizie for performance, but others should optimize for simplicity, and I think that is the part that is missing.

Maybe this is a good analogy. Let's say that a studio apartment is just a plain Apache server that serves HTML. A one bedroom apartment is a SSG. A two bedroom apartment is just a vanilla SSR framework that handles requests, hits a database, and returns HTML. Then 3+ bedroom houses start to incorporate features like filesystem based routing, partial hydration, client side navigation after the on subsequent navigations, deciding between SSG and SSR per route, etc.

I feel like I am looking for a two bedroom apartment, but there are non available in the market. It's all four and five bedroom houses that are listed. Ok, I guess there are some two bedroom apartments that are listed. I actually found one and got really excited at first, but on closer inspection it was old and unmaintained and had various warts.

SSG

With SSR, the request comes in, the server parses it and generates HTML by running stuff through some sort of view layer or templating engine, and then sends that HTML back as the response. With SSG, the HTML is basically generated at build time instead of runtime. So when the request comes in, the HTML is already there. It doesn't need to get generated when the request comes in. So SSG saves you the time it takes to generate the HTML.

I don't really care though. I can't imagine this actually improves performance all that much, so I'd prefer if SSG just wasn't a feature that was offered in the first place.

Client vs server-side navigation

I'm not sure if this is actually the right vocabulary or not, but let's distinguish between rendering and navigation. Rendering is the first time you visit the site and navigation is subsequently when you click a link. So like if you visit example.com for the first time, it is rendered, and then when you click "About" to navigate to example.com/about, it is a navigation.

Frameworks are moving towards server-side rendering with client-side navigation. So initial page load is server rendered (unlike SPAs) whereas subsequent navigations happen client-side.

I see the same tradeoff of performance vs simplicity. Client-side navigation helps with performance, but at the cost of simplicity. To me, I default to simplicity and put the burden of proof on performance to change my mind. Here, I don't feel like performance has made a strong enough argument. Sending a request to a server and getting back the full page in HTML is fine. It seems like it is fast enough for the great majority of situations, so it is my personal preference.

Partial vs full hydration

With server side rendering, the server responds with HTML. But what about interactivity? Event listeners and stuff? That's where hydration comes in. After the server responds with HTML, some JavaScript is then loaded that goes through the DOM and attaches event listeners and stuff. That might be a slightly inaccurate description, but I think it's sufficient for now.

You could traverse through the whole DOM to do hydration stuff, but frameworks like Astro and Fresh utilize the islands architecture and say "Hey, we know that the navbar is the only interactive part of this web page, let's only traverse through that part of the DOM to do hydration stuff". So basically, it lets you avoid unneeded work.

But again, this doesn't come for free. It comes at the expense of complexity. And to me, the performance gains are much too small to justify the added complexity. Few sites need that level of performance.

Filesystem based routing

This I don't see as a performance vs simplicity thing actually. I guess it's more of a personal preference. My personal preference is to not have filesystem based routing, although it's not a strong one.

In short, filesystem based routing potentially leads to headaches, and the benefits are either unclear and/or small, so I'd just prefer to avoid it.

Options

So with all of that said, let's see if we could find a framework that fits my needs.

react-ssr

All I really want is to have a Node + Express backend, Express would handle the routing, and you'd be able to do something like res.render('account', { user: user }) which would render src/pages/account/index.tsx. And there'd also be full hydration so we can have interactivity wherever we want. Yeah. That's all I actually want from a framework I think.

Well, this little framework react-ssr with only 240 GitHub stars offers that.

I messed around with it though and ran into some issues. It hasn't been maintained for 2+ years so various packages are old. This gave me some headaches getting set up. I was able to get past most of them, but not all, including not being able to get Chakra working. I need to have Chakra, and depending upon an unmaintained framework scares me, so as much as it pains me to say this, I don't think react-ssr is the answer.

Fresh

Fresh from Deno looked quite cool to me at first. But after looking more closely, there are issues.

Astro

Like Fresh, I really like how Astro doesn't try to mess around with client side navigation. It's just straight SSR. Cool. But they also use that islands architecture with partial hydration, which comes with similar issues.

And you need to at least start off using these .astro files that use a sort of custom version of HTML. From there you could import React components, but things have to start with with .astro file. This is pretty limiting to me. Chakra needs to be with React, so basically I'd have all of these .astro files that are just light wrappers around React components. It's just excessive and ugly to me.

Remix

We use Remix at work. It's ok, but I don't love it. I don't want the client-side navigation. I don't like useFetcher and other hooks they want you to use to make HTTP requests. Their version of filesystem based routing is pretty limiting.

Solid Start

Solid Start seemed really cool at first. No messing around with client-side navigation or partial hydration. They have filesystem based routing, but their specific implementation seems better than others. Everything is just pretty straightforward.

But then I realized that Chakra doesn't work with Solid. I also realized that Solid can actually be pretty annoying to work with due to components only rendering once. I'd just prefer React. Sigh.

NextJS

This leaves us with what I've been trying to avoid: NextJS. Next just feels too bloated to me. They try to offer all of these little performance optimizations in ways that feel overly complicated to me. They don't deal with partial hydration so that's good, although they kinda do now in Next 13 with server vs client components. There's some complexity there. And they really go hard with client-side navigation. They try to do SSG by default and you have to opt out of it if you want to. Another thing for you to keep your minds eye on. And like almost every other framework, it offers filesystem based routing that I'd prefer to avoid.3

But there are also good things about Next. It is super popular and well maintained. The docs are pretty good. It works with Chakra. It works with Prisma. Well. The docs for both are excellent. It uses Turbopack as a build tool which is supposed to be super fast and awesome. And NextAuth.js is supposed to be pretty awesome.

I'm sad because I wasn't able to find a framework that gives me that radical simplicity I was looking for, but being pragmatic, I think Next is probably the winner.


If you have any thoughts, I'd love to discuss them over email: adamzerner@protonmail.com.

If you'd like to subscribe, you can do so via email or RSS feed.

  1. Plus it'd be fun! Not to offer fake wisdom, but being fun is something that is both important and underrated.↩

  2. Well, usually that's the question. If you have some sort of specialized needs you might want to use something more niche like a graph database.↩

  3. I'm also turned off by how sales pitch-y their Next 13 Keynote was. Gross. It genuinely could have passed for a The Onion skit making fun of Apple.↩

#code