Speak text in a Next.js app with a route handler
A server route handler that calls the speech API with your key, plus a client button that plays the returned link.
The key lives in a server environment variable. The browser only ever calls your own route.
1. Route handler
app/api/speak/route.ts
export async function POST(req: Request) {
const { text } = await req.json();
const clean = String(text ?? "").slice(0, 500);
if (!clean) return Response.json({ error: "text_required" }, { status: 400 });
const r = await fetch("https://speakvora.com/api/v1/speak", {
method: "POST",
headers: { "x-api-key": process.env.SPEAKVORA_KEY!, "content-type": "application/json" },
body: JSON.stringify({ text: clean, voice: "af_heart", lang: "en" }),
});
const j = await r.json();
return Response.json(r.ok ? { url: j.url } : { error: j.error }, { status: r.status });
}2. Environment
.env.local (never commit)
SPEAKVORA_KEY=your_key_here3. Client button
app/Say.tsx
"use client";
export function Say({ text }: { text: string }) {
async function play() {
const r = await fetch("/api/speak", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text }) });
const j = await r.json();
if (r.ok) new Audio(j.url).play();
else console.error(j.error);
}
return <button onClick={play}>Speak</button>;
}Before production
- Rate-limit your own route: it spends your quota. See errors and retries.
- Cache the returned link per sentence (guide).
- Keep the key out of client bundles: only variables prefixed
NEXT_PUBLIC_reach the browser, so do not use that prefix.