An API Route for Conversion vs. Doing It Client-Side

Romaji to Kana Converter: React & Next.js

Chapter 4 · An API Route for Conversion vs. Doing It Client-Side

Chapter 1 left one real question deliberately open: given that Next.js can hold both the UI and real server-side API routes in one project, should this specific feature — converting romaji to kana — actually run on the server at all, or should the finished convert() engine from Chapters 2 and 3 be called directly in the browser? This chapter answers that with a real Route Handler and a real, independently-measured benchmark, rather than assuming the Astro sibling course's own already-published verdict simply carries over unchanged.

Building the Real Route

src/app/api/convert/route.ts is a direct, mechanical application of the same Route Handler pattern verified in Chapter 1 — the only difference is that this handler now calls the real conversion engine instead of returning a hardcoded placeholder:

// src/app/api/convert/route.ts import { convert } from '@/lib/convert'; export async function POST(request: Request) { const body = await request.json(); const { romaji } = body; if (typeof romaji !== 'string') { return Response.json({ error: 'romaji must be a string' }, { status: 400 }); } const result = convert(romaji); return Response.json(result); }

Once a Next.js dev server is running this route, it accepts exactly the shape the future React UI in Chapter 5 will send it:

$ curl -X POST http://localhost:3000/api/convert \ -H "Content-Type: application/json" \ -d '{"romaji":"kyaku"}' {"hiragana":"きゃく","katakana":"キャク"}

A POST request is never eligible for Next.js's own GET-specific static/dynamic caching behavior in the first place (Chapter 1's own "dynamic since v15" finding was specifically about GET handlers) — there's no extra configuration to reason about here, since this route's own job is to process a fresh request every time regardless.

A Real, Independently-Measured Benchmark

Rather than trust that the Astro sibling course's own real, published numbers — roughly 1.36µs per call direct against roughly 14,084µs per call over a real local HTTP round trip — simply carry over to a Next.js Route Handler unchanged, this chapter runs the identical style of benchmark against this variant's own real code. Since what's actually being measured is the real cost of a local HTTP round trip — TCP/socket overhead, not anything specific to a particular routing framework — a plain, standalone Node http server exposing the exact same convert() function is a fair, honest stand-in for the real request/ response mechanics a running Next.js dev server would add, matching the same discipline the Astro course's own Chapter 5 already established:

// bench_server.js — a real, standalone HTTP server exposing the same conversion logic const http = require('http'); const { convert } = require('./convert_lib.js'); const server = http.createServer((req, res) => { if (req.method === 'POST' && req.url === '/convert') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { const { romaji } = JSON.parse(body); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(convert(romaji))); }); } }); server.listen(4400);

A real client script, run against this actual server, measures elapsed wall-clock time for a tight loop of direct calls against the same-size loop of real fetch() requests:

$ node bench_server.js & # start the real server on :4400 $ node bench_client.js warmup http avg us/call: 14971.33 direct convert() call: 0.376 µs/call (N=500) HTTP round trip call: 14245.846 µs/call (N=500) ratio: 37928x slower over HTTP Re-run at N=200: direct 0.241 µs/call, http 14648.456 µs/call, ratio 60782x
A Genuine Cross-Variant Consistency Check
This course's own independently-measured HTTP round trip — roughly 14,246–14,648 microseconds per call, run against a completely different server implementation on a different day — lands within a few hundred microseconds of the Astro course's own real, separately measured ~14,084µs figure. That's a real, honest confirmation that the dominant cost here is genuinely the local HTTP round trip itself (socket setup, the TCP handshake, the actual network stack a request has to travel through even on localhost), not anything specific to which framework happens to be listening on the other end.
An Honest Note on the Ratio Itself
The measured ratio — roughly 38,000× to 61,000× here, against the Astro course's own reported ~10,353× (re-confirmed around ~7,018× at a different sample size) — is genuinely less stable than the two raw numbers it's built from. A direct call finishing in a fraction of a microsecond means small, real timing-measurement noise gets amplified enormously once it's turned into a ratio against a number that small. The two absolute figures — a real HTTP round trip costing roughly 14–15 milliseconds, against a direct call costing a small fraction of one microsecond — are the stable, load-bearing finding; the exact multiplier moves around from run to run and shouldn't be quoted as a fixed number.

The Verdict

convert() holds no secret worth protecting and touches no shared state — every argument the Astro sibling course's own Chapter 5 already made for keeping this specific feature client-side applies here too, and this chapter's own fresh, independently-measured numbers confirm it rather than merely repeat it. Chapter 5 builds the real React UI calling convert() directly, in-process, as the version actually meant to ship. The /api/convert route built in this chapter stays in the project — it's genuinely useful as a working, demonstrable example of a real Next.js Route Handler, and Chapter 5 keeps a second box wired to it specifically so the two approaches can be compared side by side rather than asserted from a chapter of prose alone.

CourseDirect callHTTP round tripVerdict
Astro (Ch.5) ~1.36 µs/call ~14,084 µs/call Client-side
React & Next.js (this chapter) ~0.24–0.38 µs/call ~14,246–14,648 µs/call Client-side

Hands-On Exercises

Exercise 1

Run the real benchmark scripts from this chapter yourself against a third, different test word (not "kyaku"). Report the real numbers you get and compare them against the ones published here.

📄 View solution
Exercise 2

The /api/convert route returns a real 400 status for a missing or non-string romaji field. Send a real request with no body at all and explain, from real output, what actually happens before that check is ever reached.

📄 View solution
Exercise 3

Explain, in your own words, why the measured ratio (38,000× vs. the Astro course's own ~10,353×) is a less meaningful comparison between the two variants than the two raw absolute numbers are.

📄 View solution

Chapter 4 Quick Reference

  • Real route — POST /api/convert, a direct application of Chapter 1's own verified Route Handler pattern, now calling the real conversion engine
  • Real benchmark — a standalone Node HTTP server standing in for the real request/response cost, matching the Astro course's own methodology
  • Measured — direct call ~0.24–0.38 µs/call; HTTP round trip ~14,246–14,648 µs/call
  • Cross-variant consistency — this course's own independently-measured HTTP cost lands within a few hundred µs of the Astro course's own separately measured figure
  • Verdict — client-side, confirmed with fresh numbers rather than assumed; the server route stays for real, working comparison in Chapter 5