What I Learned Building an Astro Book Review App (And Almost Breaking My Terminal Twice)

Retro computer terminal illustrating lessons learned building an Astro blog with Vite and npm
Building an Astro blog turned into a hands-on lesson in Vite, dev servers, terminal processes, build tools, and modern JavaScript frameworks.

I recently started building a small book review site with Astro, and along the way I ended up down a rabbit hole of dev servers, bundlers, and stuck terminal processes. What started as “why won’t my server start” turned into a crash course on how modern JavaScript frameworks actually work under the hood. Here’s the breakdown, minus the panic.

What Is Vite, Actually?

If you’re using Astro (or Vue, Svelte, or newer versions of React tooling), you’ve probably seen this line pop up in your terminal:

[vite] connected.

Vite (pronounced “veet,” French for “fast,” and also the source of at least one internal debate on how to pronounce it correctly) is the build tool running quietly underneath Astro. It handles three main jobs:

  • Serving your dev server — the thing running at localhost:4321
  • Hot Module Replacement (HMR) — instantly pushing your saved changes to the browser without a full reload
  • Bundling — packaging everything into optimized files when you build for production

The key thing that makes Vite fast is that it doesn’t bundle your entire app before serving anything in development. It leans on native ES Modules — a feature modern browsers now support natively — so the browser itself can request individual files via import statements, and Vite just hands over exactly what’s asked for, transformed on the fly. Nothing gets pre-packaged until you’re ready to actually ship.

Bundler vs. Build Tool (Yes, There’s a Difference)

These two get used interchangeably a lot, but technically:

  • Bundler — the part that specifically combines many files into fewer, optimized files. Examples: Rollup, esbuild, Webpack, Turbopack.
  • Build tool — the whole pipeline: dev server, HMR, transpiling, bundling, the works. Examples: Vite, Webpack (it’s both).

So Vite is the build tool. Underneath it, it uses esbuild for fast dev pre-bundling and Rollup for the final production bundle. Basically, Vite is the restaurant, and Rollup/esbuild are the kitchen staff actually cooking the food.

“Another Dev Server Is Already Running” — A Very Normal Mistake

At some point I ran npm run dev and got this instead of my usual server:

Another astro dev server is already running.
URL: http://localhost:4321
PID: 95729

Not an error, just Astro politely telling me I already had a server running somewhere (probably an old terminal tab I’d long forgotten about, RIP). You’ve got a few options here:

  • Just use the existing server — open the URL, it’s already live
  • Stop it cleanly: npx astro dev stop
  • Or force-replace it: npm run dev -- --force

I stopped it, ran npm run dev again, and all was well. For about ten minutes.

Then I Accidentally Ran build Instead of dev

Muscle memory is a powerful and occasionally embarrassing thing. Instead of starting my dev server, I kicked off a full production build mid-project. No real damage done — npm run build just compiles an optimized version of your app into a .next or dist folder. It doesn’t touch your source code. It just wastes a few seconds of your life and mildly damages your ego.

Then, mid-build, I hit Ctrl+Z assuming that would kill the process. It did not. Ctrl+Z suspends a process — it pauses it in the background rather than ending it. My terminal helpfully confirmed this:

zsh: suspended  npm run build

Running jobs revealed I actually had two suspended processes stacked up from earlier sessions I’d forgotten about — an old npm run dev and the build. Turns out terminal tabs are basically junk drawers if you’re not careful.

The fix was simple:

kill %1
kill %2
jobs   (should now return nothing)

Lesson learned: if you actually want to stop a running process, use Ctrl+C. Save Ctrl+Z for when you genuinely want to pause and resume something later with fg.

Compile-Time vs. Runtime: The Concept That Explains Basically Everything

Retro developer terminal showing Astro and Vite dev server commands, build processes, and compile-time concepts
Astro’s development workflow uses Vite for the dev server, HMR, and build tooling while terminal commands control development and production processes.

This is the part that actually reframed how I think about frontend frameworks. The big divide between them isn’t really about syntax — it’s about when each framework figures out what changed on your page.

  • Compile time — decided in advance, before the code ever reaches the browser (during your build step)
  • Runtime — decided live, in the browser, while the user is actually using the app

Think of it like meal prepping on a Sunday versus cooking to order in a restaurant. One does the thinking ahead of time and just hands over the finished plate. The other figures it out live, with the customer sitting right there watching.

Here’s how that plays out across the major frameworks:

FrameworkReactivity ComputedShips a Runtime?Update Mechanism
AngularJS (old)RuntimeYes, heavyDirty checking — loops through everything, over and over, until nothing’s changed
Angular (new)Runtime, compiler-assistedYesChange detection triggered by events, optimized by the Ivy compiler
ReactRuntimeYesVirtual DOM diffing — builds a copy in memory, compares, patches what’s different
Next.jsRuntime (it’s React underneath)YesSame as React, plus optional server-side pre-rendering of the HTML shell
VueMostly runtime, plans ahead a littleYes, lighterProxy-based tracking, combined with compiler-optimized diffing
SvelteCompile timeMinimal to noneDirectly updates exact DOM nodes — no comparing, no guessing
AstroN/A — mostly staticNone by defaultShips plain, finished HTML. JS only loads for parts you explicitly mark interactive

Svelte does its thinking the night before and just hands in finished homework. React figures it out live at the desk while the teacher watches. Astro, most of the time, skips the assignment entirely and just hands you a printed page.

Quick Detour: What’s an ORM?

Since we’re already deep in the weeds, worth a quick mention — an ORM (Object-Relational Mapping) tool lets you talk to a database using your programming language’s normal objects instead of writing raw SQL by hand.

// Without an ORM
SELECT * FROM books WHERE author = 'Tolkien';

// With an ORM (Laravel's Eloquent)
Book::where('author', 'Tolkien')->get();

It’s basically a translator between “how your code thinks” (objects) and “how your database thinks” (tables and rows), saving you from writing repetitive SQL and accidentally leaving the door open for SQL injection.

Bonus Round: PHP Frameworks, for the Backend Curious

Since Laravel came up, here’s the honest 2026 pecking order for PHP frameworks:

  1. Laravel — the productivity king. Batteries included, huge ecosystem, best for shipping fast.
  2. Symfony — a toolbox of 50+ reusable components rather than a finished app. Built for long-term, large-scale structure.
  3. CodeIgniter — lightweight and quick to set up, great for smaller projects or prototypes.

Interestingly, Laravel actually builds on top of core Symfony components — the same relationship Next.js has with React: a batteries-included framework layered on top of a more minimal, assemble-it-yourself foundation.

The Actual Takeaway

None of this was strictly necessary to build a book review blog. But understanding why your dev server behaves the way it does — and what’s actually happening between hitting save and seeing your change on screen — makes debugging a lot less mysterious and a lot more “oh, that makes sense” the next time something suspends itself in your terminal for no apparent reason.