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:

# from the Angular project root ng generate environments
Verified Against Angular's Own Current Documentation
Older Angular versions generated 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:

// src/environments/environment.ts — the default, used for a real production build export const environment = { apiBaseUrl: '', }; // src/environments/environment.development.ts — swapped in automatically for development export const environment = { apiBaseUrl: 'http://localhost:4000', };
// angular.json — the fileReplacements block ng generate environments writes automatically "configurations": { "development": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.development.ts" } ] } }

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:

// src/app/convert.service.ts import { environment } from '../environments/environment'; // ... switchMap((romaji) => this.http.post<ConvertResponse>(`${environment.apiBaseUrl}/api/convert`, { romaji }).pipe( // ...catchError, unchanged from Chapter 6... ) )
One File, Not Six
Compare this against a typical multi-component frontend, where the identical fix has to be repeated in every file that independently calls the backend. That was never a risk here — Chapters 4 through 6 already concentrated every real network call behind this one service, so hardening it for deployment costs exactly one line changed in exactly one place.

Building the Frontend for Production

# from the Angular project root (named "romaji-converter" since Chapter 1) ng build
A Real, Easy-to-Miss Detail: the Output Nests One Level Deeper Than Expected
Current Angular's default build uses the esbuild-based application builder, which writes its output to 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

// server.js — added after the /api/convert route const path = require('path'); app.use(express.static(path.join(__dirname, '../romaji-converter/dist/romaji-converter/browser')));

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.

No SPA-Fallback Route Needed — for an Even More Fundamental Reason Than Usual
Most single-page-app deployment guides need one more piece here: a catch-all route serving 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:

// server.js app.use(cors({ origin: process.env.ALLOWED_ORIGIN || 'http://localhost:4200', }));
Set ALLOWED_ORIGIN Once a Real Domain Exists
In production, 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

# installed once, globally, on the server npm install -g pm2 # start the app under PM2's own supervision pm2 start server.js --name romaji-converter-api # persist the process list across a real server reboot pm2 save pm2 startup

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:

# /etc/nginx/sites-available/romaji-converter server { listen 80; server_name yourdomain.com; location / { proxy_pass http://localhost:4000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

Trying It

# build the frontend cd romaji-converter && ng build && cd .. # start the backend, now also serving the built frontend cd romaji-converter-api && node server.js

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

Exercise 1

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 solution
Exercise 2

Run 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 solution
Exercise 3

Wire 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 solution

Chapter 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