How I build products
Learn how I build products. From using Bun for everything from APIs to scripts to staying away from NextJs.
This blog is an extenstion of a talk I gave at the a Vancouver.dev event on the 22nd of October 2025. If you are looking for the slides you can find them here.
In this talk I covered how I built Teardown, the tech stack I used and how I architected the codebase. This blog is an extension of that talk.
When building a product or service, there are a few things I look at to ensure I have a strong foundation from the start.
- Repo structure - how is the code organized? Monorepo, Polyrepo, Flat, etc.
- Frontend - what is the user interface. Vite / Tanstack, NextJs
- Backend - what is the server-side logic, data processed and API routes. Bun, Express etc.
- Database - where is the data being stored? Prostgres, Firebase, Supabase etc.
- API - how is the data being exchanged between the frontend and backend?
This might be obvious for some. But when your first starting out you really have no idea.
Repo structure
This is kinda the first thing in any project. Its the foundation that everything else is built on top of.
There are pros and cons to any approch and but really with the age of AI and LLMs having everything in one monorepo just makes so much sense now.
To do this I use Bun Workspaces.
My initial directory structure would normally look something like this:
my-project/
├── scripts/ # Shared scripts
├── apps/ # Deployable applications
│ ├── backend/ # API server
│ └── web/ # Landing page app
│ └── dashboard/ # Dashboard app
└── packages/ # Shared libraries
├── types/ # Shared TypeScript types
├── sdk/ # API client
└── ui/ # React components
└── styles/ # Shared styles
└── tsconfig/ # Shared TypeScript configThis is pretty self explanatory. But just to break it down:
scripts/- Shared scripts, running dev, build, test, lint, format, etc...
apps/- Place any deployable applications here. Web apps, mobile apps, APIs etc... This is where you will have your main application code lives.
packages/- Place any shared libraries here. This includes shared types, SDKs, UI components, styles, etc... This is where you will have your shared code lives.
Most of the time there will be at least one app, if not more. ( Dashboard, App, Admin etc..)
With the packages I will typically have a few shared types, SDKs, UI components that are alomost the same in every project, just with a few minor project specific adjustments.
If you want to learn how to implement Bun workspaces you can check out my how to guide here.
Scripts
Scripts are always going to be a part of any project so making sure patterns are in place for engineers to follow is key.
For this I also use Bun, and use the Bun runtime to execute the scripts.
The reason I chose Bun again here is because I could write my scripts using typescript and interact with the shell system using their $ directive. docs
For example I sometimes use Supabase, there is not script to easily generate the types for the supabase project. But there is a CLI from supabase.
So combining the two, you can write a scripte like this to generate the types for the supabase project.
#!/usr/bin/env bun
import { $ } from "bun";
const PROJECT_ID = "<<YOUR_PROJECT_ID>>";
const GENERATED_TYPES_FILE = "./src/generated.types.ts";
// Remove the previous types file if it exists
await $`rm -f ${GENERATED_TYPES_FILE}`;
// Generate updated types from Supabase
await $`supabase gen types typescript --project-id=${PROJECT_ID} --schema=public,v1 > ${GENERATED_TYPES_FILE}`;
// Automatically update the git repo (optional)
await $`git add ${GENERATED_TYPES_FILE}`;
await $`git commit -m "chore: update generated types from supabase"`;When you run this script it will generate the types for the supabase project supplied and commit them to the repository on the branch being ran from.
You could even extend this so it runs automatically before a new PR is merged through a GitHub Action and supply the project ID via secrets.
Frontend
When it comes to the frontend, I typically use TanStack Start, a Full-stack Framework powered by TanStack Router for React.
Tanstack Start is super powerful and easy to use. People typically ask me why I chose Tanstack Start over NextJs - I find NextJs to be a bit too opinionated and restrictive for my needs.
Im not a fan of the verbose directives (use client, use server, ...) makes building a pain cause your never really sure what is going on cause you cant really get a mental model of what is happening.
NextJs has so many foot guns, I just didn't want to deal with them. I don't know how many reddit posts ive seen of people complaining about a $10k bill from Vercel, becuase they did something wrong and shot themselves in the foot.
With Tanstack Start you dont get any of that. You get a clean, simple, and predictable file structure along with strong type safety and DX. The devtools for all of Tansatck packages are also great and make it easy to debug and inspect your apps.
Backend
When it comes to the backend, i've recently landed on Elysia & again Bun.
Using the Bun runtime I compile the typescript code into a single binary file which makes executing multiple instances super easy.
This also means I can ignore all those nasty node_modules and just have a single binary file to deploy. Elysia & Bun just makes it super easy to do that.
Now lets talk about monolith vs microservices.
Monoliths are fine, and really I encourage people to start with one. Microservices are a complex beast and should be used when you need to have a very complex application that needs to be split up into smaller modules.
Elysia can handle about 2,454,631 reqs/s on a single instance. Thats a lot of requests. Obviously this is a lot of requests and it depends on the impact of the request on system resources. But its a still alot of requests.
Most applications can get away with a monolithic backend for quite some time. But as you grow and the complexity of the application grows, you will eventually need to split it up into smaller modules.
I rarely see a monolith backend being the source of problems so its completly fine to start with.
How you may ask? well Elysia provides a lot of the functionality that you would typically find in a framework like trpc. Through a plugin called Eden Treaty which provides you a way to create a fully type safe API with built in validation, authentication, authorization, and more.
You can see the body shape you need to supply on post requests, there is no need to create an open api schema or any other documentation.

Becuase the documentation is in the code itself. If you change a type in the backend, add an extra field, etc... the types will just flow on through to the frontend. No type generation or syncing needed. Super powerful!
Elysia also provides an additional layer called Eden Treaty which is a layer on top of Elysia that provides a lot of the functionality that you would typically find in a framework like trpc.