Why We Built This
Every project starts with a simple question: what problem are we solving? For us, it was the gap between prototyping speed and production readiness. Too many side projects die in the valley between “it works on my machine” and “it works for real users.”
We wanted a stack that let us move fast without accumulating debt. Here’s what we learned along the way.
The Stack We Chose
Choosing a tech stack is less about picking the “best” tools and more about picking tools that work well together. Here’s what we landed on and why:
- Astro for the frontend — static-first, ships minimal JS
- Vue for interactive islands — reactive without the SPA overhead
- Express on the backend — boring, reliable, well-documented
- Drizzle ORM for database access — type-safe SQL without the magic
- Zod for validation — one schema, used everywhere
The common thread: each tool does one thing well and stays out of the way.
Architecture Decisions That Paid Off
Vertical Slices Over Layers
Traditional MVC scatters a single feature across controllers, services, models, and views. When you need to change “billing,” you touch five folders. We organized by feature instead:
src/modules/
auth/
auth.logic.ts
auth.handler.ts
auth.public.ts
billing/
billing.logic.ts
billing.handler.ts
billing.public.ts
Each folder is self-contained. Adding a feature means adding a folder, not threading code through layers.
Pure Logic, Impure Shell
We strictly separate thinking from doing. Business logic lives in *.logic.ts files — pure functions, no database calls, no side effects. The handler orchestrates: fetch data, call logic, save results.
// billing.logic.ts — pure, testable
export function calculateDiscount(price: number, tier: string): number {
return tier === 'pro' ? price * 0.8 : price;
}
// billing.handler.ts — orchestration only
async function checkout(userId: string) {
const user = await db.users.findById(userId);
const price = calculateDiscount(100, user.tier);
await stripe.charge(price);
}
This made our logic 100% unit-testable without mocks.
Performance Benchmarks
We tracked key metrics across different deployment configurations. The numbers surprised us:
| Metric | Static (CDN) | SSR (Node) | SSR (Edge) | SPA (Client) | Hybrid |
|---|---|---|---|---|---|
| TTFB | 45ms | 210ms | 89ms | 52ms | 67ms |
| FCP | 0.8s | 1.4s | 0.9s | 2.1s | 1.0s |
| LCP | 1.2s | 1.9s | 1.3s | 3.4s | 1.5s |
| TTI | 1.0s | 1.6s | 1.1s | 3.8s | 1.3s |
| Bundle Size | 12KB | 45KB | 38KB | 180KB | 28KB |
| Lighthouse Score | 98 | 82 | 91 | 64 | 93 |
Static wins on almost every metric. The only trade-off is dynamic content — but for most pages, build-time rendering is more than enough.
Mistakes We Made
Not everything went smoothly. A few highlights:
- Over-engineering auth. We built a custom session layer before realizing a simple JWT + refresh token flow covered 95% of use cases.
- Premature abstraction. We created a generic “entity service” base class (ironic, given our no-classes rule) that three modules used. We deleted it after a month — each module’s needs diverged quickly.
- Ignoring mobile viewports. Tables, code blocks, and wide images broke layouts on phones. A single CSS rule (
overflow-x: auto) on content containers fixed most of it.
The best code is the code you don’t write. The second best is code you can delete without fear.
What We’d Do Differently
If we started over tomorrow:
- Start with deployment, not features. Get a “Hello World” live on day one. Every day without a deploy pipeline is a day you’re building assumptions.
- Write less, validate more. Zod schemas at the boundary catch more bugs than unit tests in the middle.
- Measure before optimizing. We spent a week optimizing a database query that ran twice a day. Meanwhile, a missing index on a high-traffic endpoint went unnoticed.
Wrapping Up
Building software is a series of trade-offs. The goal isn’t perfection — it’s making decisions you can live with six months from now. Pick boring tools, organize by feature, separate logic from I/O, and ship early.
The best architecture is the one your team can understand, modify, and debug at 2 AM when something breaks in production.