Deployment
Romaji to Kana Converter: Astro
Chapter 7 · Deployment
This project has a genuinely different shape from almost every other Astro course on this site: it's a
static site by default (Chapter 1's own real, deliberate choice), with exactly one route — Chapter 5's own
/api/convert — opted into on-demand rendering. Deployment has to account for both halves of
that hybrid at once, and this chapter's own real headline finding is that Astro's build tooling already
handles the split cleanly, with no extra configuration needed to stitch the two together.
Building for Production: Two Outputs From One Command
Verified directly against Astro's own current documentation for the @astrojs/node adapter: once
an adapter is installed and at least one route opts into on-demand rendering, astro build
produces two real, separate outputs from a single command — exactly the split this project's own hybrid
setup needs:
One Server Handles Both Jobs
A genuinely convenient real detail, confirmed directly against Astro's own documentation: in
mode: 'standalone' — already set back in Chapter 5 — "the server handles file serving in
addition to the page and API routes." The one built server doesn't just answer /api/convert; it
also serves every file sitting in dist/client/ directly, with no separate static file server,
CDN, or extra configuration required to wire the two together:
HOST and PORT are read directly by the built server itself — Astro's own
documentation is explicit that neither Astro nor the adapter loads environment variables on its own behalf,
so anything genuinely secret would need its own loading mechanism; this project has no secrets at all to
worry about.
node ./dist/server/entry.mjs command — but its
entire site is genuinely dynamic, so output: 'server' puts every single page through that one
Node process. This project runs the exact same real command for a fundamentally different reason: almost
the whole site is static and could be served with zero server process at all — the one Node process here
exists purely to keep Chapter 5's own real server-side comparison route reachable, not because the site
genuinely needs a server to function.
PM2 for Process Management
A single, long-lived process, kept alive and restarted automatically if it ever crashes — there's no shared database, no connection pool, and no multi-writer question anywhere in this project (unlike Premier League Predictor: Astro's own real SQLite-file concurrency question), so there's no real reason to run more than one instance at all:
nginx Reverse Proxy
A standard proxy in front of the one running process — plain proxy_pass, nothing route-specific
needed yet, since the same process already correctly serves both static files and the API route on its own:
A Real Rate Limit — Specifically for /api/convert
Every other route on this project's own site is free, in every real sense — a prerendered static file costs
the server nothing at all to serve beyond a plain file read. Chapter 5's own real, measured finding changes
that story for exactly one route: each real request to /api/convert costs the server roughly
14 milliseconds of genuine processing time, compared to Chapter 4's own client-side box, which costs the
server nothing whatsoever. That specific route — and only that one — is genuinely worth protecting from a
real flood of requests:
rate=10r/s allows a sustained average of 10 requests per second from any one client IP (tracked
via $binary_remote_addr); burst=20 nodelay lets a genuine short spike of up to 20
requests through immediately rather than queuing them, with anything past that rejected outright rather than
delayed. limit_req_status 429 overrides nginx's own real default of 503 — a
deliberate choice here, since 429 Too Many Requests is the more semantically accurate real HTTP
status for "you're sending requests too fast," verified directly against nginx's own documentation.
No equivalent limit is placed on the static routes at all — there's no real cost to protect there, and adding one would just be friction with nothing genuine behind it.
A Real, Honest Closing Reflection
This entire chapter exists to keep one specific route reachable and correctly deployed — a route Chapter 5's
own real, measured evidence already concluded shouldn't be this app's own default. That's worth stating
plainly rather than glossing over: if this project were being deployed purely as a production feature rather
than as a working, comparable example for this course, the honest, evidence-based choice would be to drop
/api/convert, the Node adapter, and this entire chapter's own PM2/rate-limiting setup
altogether — deploying as nothing more than a folder of static files with zero server process required at
all. This chapter builds out the fuller deployment anyway, specifically because the server-side comparison
route is worth keeping reachable for anyone reading this course to try both approaches for real, not because
the finished app actually needs it.
Hands-On Exercises
Build the project, run the standalone server, and confirm — with a browser and with a real curl POST request — that both index.astro's own static page and the on-demand /api/convert route are served by the exact same running process on the exact same port, with no separate static file server involved.
📄 View solutionAdd the nginx rate-limit configuration exactly as this chapter describes, then send a real rapid burst of more than 20 requests to /api/convert from a script. Confirm some requests succeed and later ones are rejected with a real 429 status, matching the limit_req_status override.
📄 View solutionGiven Chapter 5's own real, measured verdict that client-side conversion is objectively better for this feature, explain in your own words why this chapter still builds out a full deployment story for the server-side route rather than simply recommending it be deleted.
📄 View solutionChapter 7 Quick Reference
- Two outputs, one build — dist/client/ (static pages) and dist/server/entry.mjs (the one on-demand route), verified against Astro's own current documentation
- One process, two jobs — standalone mode's built-in file serving means the same server handles both static files and /api/convert, no extra config needed
- PM2, single fork instance — no database, no multi-writer question, no real reason to run more than one
- nginx rate limit on /api/convert only — real, targeted protection for the one route with a genuine per-request server cost (Chapter 5's own measured ~14ms), left off every free static route
- 429, not 503 — a deliberate limit_req_status override to the more semantically accurate real HTTP status code
- The honest reflection — this whole chapter deploys a route Chapter 5's own evidence already said shouldn't be the default, kept alive specifically so it stays reachable for comparison
- Next chapter: Capstone — shipping this as a real new page on the live site, and scoping kanji conversion as a genuine future sub-project