Deployment
Romaji to Kana Converter: Angular & Express
Chapter 7 · Deployment
Getting this stack running as a real, standalone deployment starts from a genuine architectural advantage.
This site's own React-based courses each end up with a hardcoded localhost:4000 scattered across
five or six separate files, one per component that ever calls the backend directly. This course never had
that problem to begin with — ConvertService has been the only thing that talks to
Express since Chapter 4, so there is exactly one hardcoded URL anywhere in this project: one line, inside one
constructor.
A Single Source of Truth for the API's Own Base URL
Angular's own real mechanism for this is environment files — but current Angular doesn't scaffold them automatically the way older tutorials assume:
src/environments/environment.ts and
environment.prod.ts automatically as part of ng new itself. Current Angular does
not — by default no environment files exist and no file replacements are configured at all; running
ng generate environments explicitly is what creates the src/environments/ folder
and wires the real replacement logic into angular.json. A project that skipped this step
entirely simply has nowhere to put an environment-specific value like this one.
The command creates two files and one entry in angular.json:
ng serve already runs under the development configuration by default, so
environment.development.ts applies with no extra flag needed. A plain ng build uses
the production configuration by default instead — no replacement happens, and
environment.ts's own empty apiBaseUrl is what actually ships. Exactly the same
empty-string-becomes-a-relative-path technique this site's own React-based courses use: a request built from
${apiBaseUrl}/api/convert with an empty base is just /api/convert, which resolves
against whatever real origin the built app happens to be served from.
The One Real Code Change
ConvertService's constructor is the only place this URL ever appears. One import, one string
swapped for a template literal:
Building the Frontend for Production
dist/romaji-converter/browser/ — not dist/romaji-converter/ directly. The extra
browser/ segment exists to leave room for a separate server-side bundle alongside it on projects
that use server-side rendering; this project doesn't, but the folder still gets created. Pointing a static
file server at the wrong level here produces a very literal, very confusing "can't find index.html" failure.
Serving the Built Frontend From Express
Registration order doesn't matter here, for the same reason it didn't in this site's own React-based
deployment chapters: express.static() only ever intercepts a request that matches a real file it
actually has — a request for /api/convert never matches anything inside that folder, so it falls
straight through to Chapter 5's own route regardless of which middleware was registered first.
index.html for any URL that isn't a real static file, so refreshing the browser on a client-side
route doesn't 404. This site's own Personal Catalogue: React, Express & MongoDB course
reached that same conclusion because its own app never happened to add a client-side route. This project's
reason is stronger still — Chapter 1 explicitly skipped Angular's router entirely (ng new with
no routing flag), so there is no client-side route that could ever exist to need a fallback for in the first
place. A single request for / already resolves to index.html automatically, and
that's genuinely the only page this app has.
Tightening CORS for Production
Chapter 1's app.use(cors()) allows a request from any origin. Once Express serves both the API
and the built frontend from the same process, the app's own requests are same-origin already and don't
strictly need CORS to succeed — restricting it explicitly is still cheap, real defense in depth:
ALLOWED_ORIGIN should point at the real deployed domain, not the local dev-server
default. Chapter 8's own capstone may end up mounting this app at a path under the user's existing Astro
site's own domain rather than a separate one entirely — whichever it turns out to be, this is the one setting
that needs updating to match.
Keeping the Server Running: PM2
pm2 logs romaji-converter-api tails real, live output; pm2 restart romaji-converter-api
picks up a fresh ng build or a server-side change without a full stop-and-start cycle.
A Reverse Proxy for a Real Domain and HTTPS
Node's own process doesn't need to be exposed to the internet directly. A real nginx reverse proxy handles the public-facing domain and — following this site's own dedicated HTTPS/TLS Fundamentals course for the actual certificate setup — TLS termination:
Trying It
Visiting http://localhost:4000 directly — not port 4200, which no longer has anything running on
it — should load the real, styled converter, with every request now round-tripping through a single Node
process serving both halves of the app.
Hands-On Exercises
Run ng generate environments in a fresh copy of the project (or a scratch project if you don't want to touch the real one yet), and inspect the real generated files and angular.json changes. Confirm environment.ts and environment.development.ts exist with the shape this chapter describes, and confirm the fileReplacements block was added automatically under the development configuration.
📄 View solutionRun ng build for real and inspect the actual output folder structure. Confirm index.html and the hashed JS/CSS bundles live under dist/romaji-converter/browser/, not directly under dist/romaji-converter/. Then deliberately point express.static() at the wrong (too-shallow) path and confirm what error actually results when you request the page.
📄 View solutionWire ConvertService to environment.apiBaseUrl exactly as this chapter describes, build the frontend, and serve it from Express at the correct path. Confirm the app works correctly when visited at http://localhost:4000 (a real production-shaped build, apiBaseUrl empty, requests resolving as relative paths) and still works correctly under ng serve at http://localhost:4200 (apiBaseUrl pointing at localhost:4000 explicitly).
📄 View solutionChapter 7 Quick Reference
- One hardcoded URL, one fix — since ConvertService has been the sole owner of every backend call since Chapter 4, deployment needs exactly one code change, in exactly one file
- ng generate environments — not auto-scaffolded by ng new in current Angular; creates environment.ts (production default) and environment.development.ts (swapped in automatically by ng serve), plus the real fileReplacements entry in angular.json
- Empty apiBaseUrl in production — the same empty-string-becomes-relative-path technique used across this site's own other deployment chapters
- A real, verified build-output detail — ng build's current esbuild-based builder writes to dist/<project>/browser/, one level deeper than dist/<project>/ alone
- No SPA fallback needed — this app was never given a router at all, so there's no client-side route to ever need one for
- PM2 + nginx — the same production process-management and reverse-proxy pattern used throughout this site's own Node-backed courses
- Next chapter: Capstone — integrating this tool into the existing Astro-based site