The Express API: Where the Conversion Logic Actually Lives
Romaji to Kana Converter: Angular & Express
Chapter 5 · The Express API: Where the Conversion Logic Actually Lives
Three earlier chapters all pointed at this exact moment. Chapter 1 decided, up front, that this variant
commits to a real Express-hosted conversion engine rather than treating client-vs-server as an open question.
Chapter 2 built that engine as a plain, framework-agnostic module and said outright that "Chapter 5 moves the
canonical copy into Express." Chapter 4 built ConvertService.convertText() around a shape
designed specifically so this move would cost nothing. This chapter cashes all three of those promises in for
real: the engine moves to Express, a genuine /api/convert route replaces the ping placeholder,
and exactly one method in the Angular app changes.
Porting conversion-engine.ts Into a Plain JavaScript Express Module
The Angular project is TypeScript by default. The Express backend, scaffolded back in Chapter 1 with a plain
npm init -y and require()-based server.js, has no TypeScript toolchain
at all — no tsconfig.json, no compile step, nothing beyond node server.js. Adding
one now, purely to preserve a handful of type annotations on one already-finished, already-tested module,
would be real, ongoing cost (a build step, a compiled-output folder, one more thing that can drift out of
sync) for a project this small.
TypeScript's own type annotations — Record<string, string>, the : string
parameter type, the : { hiragana: string; katakana: string } return type — are compile-time-only.
They're erased before the code ever runs; a TypeScript file and its own compiled JavaScript output execute
identically. Porting conversion-engine.ts into a plain .js file is a mechanical
strip-the-annotations exercise, not a rewrite:
Every other declaration in the file — the rest of KANA_MAP, both macron tables from Chapter 3,
sokuonLength, tokenizeScript, PARTICLE_OVERRIDES, and
LEXICALIZED_EXCEPTIONS — gets the identical mechanical treatment: every type annotation deleted,
every actual value left exactly as it was. Nothing worth re-pasting in full here, since none of it changed in
substance.
Verifying the Port Didn't Change Anything
A claim like "erasing types doesn't change behavior" is exactly the kind of thing this course has learned not to take on faith — Chapter 3 found two real bugs by actually running code instead of assuming it was correct. The ported module gets the same treatment, run directly against Chapter 3's own two verification sentences:
| Aspect | conversion-engine.ts (Chapters 2-4) | conversion-engine.js (this chapter) |
|---|---|---|
| Location | src/app/conversion-engine.ts — deleted below, once nothing imports it | romaji-converter-api/conversion-engine.js |
| Export style | export function convert(...) | module.exports = { convert } |
| Type annotations | Present — compile-time only, erased before running | Absent — never had any effect on what actually runs |
| Runtime behavior | Verified against every example in Chapters 2-3 | Verified above — byte-identical to the TypeScript version |
Building the Real /api/convert Route
Chapter 1's server.js already has express.json() wired up, so a POST body is
already parsed into req.body with no extra setup. A first, deliberately unguarded version —
require the ported module, read romaji straight off the request, convert it, respond:
Sending a request with no romaji field at all — the honest, realistic case of a malformed or
incomplete client request, not a contrived one:
convert(undefined) throws the moment
it tries undefined.split(' '). Verified directly against the real package that Chapter 1's own
npm install express command actually installs, with no version pinned:
Express 5.2.1, as of this writing — not the older Express 4 that plenty of still-circulating
tutorials assume. Express's own router wraps a synchronous route handler in an internal try/catch, so this
thrown error doesn't crash the whole server process — a second request right after this one still succeeds
fine — but it also doesn't stop a raw, unhelpful stack trace, real file paths included, from being sent
straight to whatever called the API.
A real check, and a clean, deliberate 400 response, close the gap entirely:
Testing It Directly, Before Angular Ever Touches It
The same two requests, against the validated route — a real, run comparison, not just an assertion that the fix works:
A clean, structured error for the bad request; the exact same byte-identical result from the earlier verification script for the good one — confirmed working correctly over a real HTTP round trip, before a single line of Angular code has been touched.
Retiring the Ping Placeholder
/api/ping already did its one job — proving Angular and Express could talk to each other, back
in Chapter 1 — and Chapter 4 already removed the only code that ever called
ConvertService.ping(). The method itself, though, was left sitting in
convert.service.ts unused. Now that a real route exists, both halves of the placeholder come out
together:
Rewiring ConvertService: From a Function Call to a Real HTTP Request
Exactly the change Chapter 4 named in advance — the import of convert is gone, and
convertText()'s two-line in-process body is replaced with a real
HttpClient call, still updating the same two signals, still returning void:
src/app/conversion-engine.ts is deleted from the Angular project outright — Chapter 2 called it
correctly back when the file was first written: this was always going to be a temporary home for it, not its
canonical one, and nothing in the frontend imports it anymore.
void return
type, same two signals updated. Neither RomajiInputComponent nor
KanaOutputComponent needed a single line changed — exactly what Chapter 4's own
finding-box promised, now genuinely delivered rather than just planned.
A Real Angular-Specific Gotcha: Cold Observables
Every backend-calling example on this site outside this course uses fetch(), whose returned
Promise starts executing the instant it's called — attaching a .then() later is optional, not a
prerequisite for the request actually firing. HttpClient genuinely doesn't work that way, and
forgetting the difference produces a real, silent failure with no error message at all:
HttpClient methods return a genuinely cold
Observable: the real HTTP call is the Observable's own producer function, and RxJS never runs a producer
function until something subscribes to it. .post(...) alone only builds a description of a
future request; .subscribe() is what actually triggers it. This is a real, well-documented
structural difference from fetch()'s own eager Promise, and exactly the kind of thing someone
used to this site's own React/Next.js-based courses would genuinely trip over here.
Confirming the Full Loop, End to End
Running npx nodemon server.js and ng serve in their own terminals and typing into
the input box reproduces the exact same visible behavior as Chapter 4 — the output panel still updates on
every keystroke. What's different underneath is real now: opening the browser's own DevTools Network tab
while typing shows a genuine POST request to /api/convert firing on every single keystroke, each
one carrying a real response this service reads straight off.
onInput() itself fires, not something this chapter's own API work needs to solve.
Hands-On Exercises
Port conversion-engine.ts into a plain conversion-engine.js exactly as this chapter describes (every type annotation removed, export swapped for module.exports, no other changes). Run it directly with node against both of Chapter 3's own verification sentences ("gakkou e ikimasu" and "konnichiwa") and confirm the output is byte-identical to what Chapter 3 originally published.
📄 View solutionBuild the real /api/convert route with the validation this chapter describes. Send it a request with no romaji field and confirm you get a clean 400 JSON error rather than a 500 stack trace, then send it a valid request and confirm the real, correct kana comes back exactly as verified above.
📄 View solutionRewire ConvertService.convertText() to call the real /api/convert endpoint exactly as this chapter describes, with no changes to RomajiInputComponent or KanaOutputComponent from Chapter 4. Confirm typing "sushi" still correctly shows すし and スシ in the output panel, and confirm in the DevTools Network tab that a real POST request now fires for each keystroke.
📄 View solutionChapter 5 Quick Reference
- The port — conversion-engine.ts moves to Express as plain conversion-engine.js; type annotations are compile-time-only, so stripping them is mechanical, and re-running Chapter 3's own test sentences confirms byte-identical output
- The real route — POST /api/convert, validated with a typeof/empty-string check before ever calling convert()
- Unguarded vs. validated, verified live — a missing romaji field crashes an unguarded handler into a real 500 HTML stack-trace leak (confirmed against the actual installed Express 5.2.1); the validated version returns a clean 400 JSON error instead
- Dead code removed — /api/ping and ConvertService.ping() both come out, now that a real route exists
- convertText() rewired — same name, same parameter, same void return type; only its internals swap a direct function call for an HttpClient POST + .subscribe(), exactly as Chapter 4 promised — zero changes to either component
- Cold Observables — forgetting .subscribe() sends no request at all, with no error either; unlike fetch()'s own eager Promise, HttpClient does nothing until something subscribes
- The real cost, named honestly — every keystroke now fires a genuine network request; softening that with a debounce is deliberately left for Chapter 6
- Next chapter: Building the Input/Output UI — the actual interface this tool has been missing since Chapter 4