Plain HTML website with a backend proxy
Run a complete HTML paraphraser while keeping your MetaPhrase key on the server.
Requires Node.js 22+. Create an empty folder with the two files below. No npm dependencies are needed.
export METAPHRASE_API_KEY='your_secret_key'
node server.mjs
Open http://127.0.0.1:4000. The Node server binds to loopback. To publish it, put it behind your HTTPS reverse proxy and set PUBLIC_ORIGIN to your website origin. Keep your site's authentication or stronger abuse controls for a public production form. This example already applies a 15/minute IP limit, caps active IP records, bounds JSON/text length and validates browser Origin.
Save as server.mjs:
import http from 'node:http';
import { readFile } from 'node:fs/promises';
const key = process.env.METAPHRASE_API_KEY;
if (!key) throw new Error('Set METAPHRASE_API_KEY on the backend');
const apiOrigin = (process.env.METAPHRASE_BASE_URL ?? 'https://metaphrase.msalman.com').replace(/\/$/, '');
const port = Number(process.env.PORT ?? 4000);
const publicOrigin = process.env.PUBLIC_ORIGIN ?? `http://127.0.0.1:${port}`;
const windows = new Map();
function reply(response, status, value) {
response.writeHead(status, {'Content-Type':'application/json', 'Cache-Control':'no-store'});
response.end(JSON.stringify(value));
}
const server = http.createServer(async (request, response) => {
try {
if (request.method === 'GET' && request.url === '/') {
response.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
response.end(await readFile(new URL('./index.html', import.meta.url)));
return;
}
if (request.method !== 'POST' || request.url !== '/api/metaphrase') {
reply(response, 404, {error:{message:'Route not found'}}); return;
}
if (request.headers.origin && request.headers.origin !== publicOrigin) {
reply(response, 403, {error:{message:'Origin not allowed'}}); return;
}
const now = Date.now();
for (const [ip, value] of windows) if (value.expires <= now) windows.delete(ip);
const ip = request.socket.remoteAddress ?? 'unknown';
let window = windows.get(ip);
if (!window) {
if (windows.size >= 10000) { reply(response, 503, {error:{message:'Please retry shortly'}}); return; }
window = {count:0, expires:now+60000}; windows.set(ip, window);
}
if (++window.count > 15) { response.setHeader('Retry-After','60'); reply(response,429,{error:{message:'Please wait a minute'}}); return; }
if (!request.headers['content-type']?.startsWith('application/json')) {
reply(response,415,{error:{message:'Send application/json'}}); return;
}
let size=0; const chunks=[];
for await (const chunk of request) {
size+=chunk.length;
if (size>120000) {reply(response,413,{error:{message:'Text is too long'}});return;}
chunks.push(chunk);
}
let payload;
try {payload=JSON.parse(Buffer.concat(chunks).toString('utf8'));}
catch {reply(response,400,{error:{message:'Invalid JSON'}});return;}
if (typeof payload.text !== 'string' || !payload.text.trim() || [...payload.text].length>20000) {
reply(response,400,{error:{message:'Enter text within the character limit'}});return;
}
const upstream=await fetch(apiOrigin+'/api/v1/paraphrase', {
method:'POST',
headers:{Authorization:'Bearer '+key,'Content-Type':'application/json'},
body:JSON.stringify({text:payload.text,mode:'standard',synonym_strength:50,stream:false}),
signal:AbortSignal.timeout(250000),
});
const data=await upstream.json();
if (upstream.headers.has('retry-after')) response.setHeader('Retry-After',upstream.headers.get('retry-after'));
reply(response,upstream.status,data);
} catch {
if (!response.headersSent) reply(response,502,{error:{message:'The writing service could not be reached. Please retry.'}});
else response.end();
}
});
server.requestTimeout=30000;
server.listen(port,'127.0.0.1',()=>console.log(`Open ${publicOrigin}`));
Save as index.html:
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>My paraphraser</title>
<style>body{font:16px/1.6 system-ui;max-width:760px;margin:50px auto;padding:20px}textarea{box-sizing:border-box;width:100%;min-height:180px;padding:12px;font:inherit}button{padding:10px 18px;margin-top:14px}pre{white-space:pre-wrap;overflow-wrap:anywhere}#error{color:#a23131}</style></head>
<body><main><h1>A fresh way to say it</h1><form id="form"><label for="text">Your text</label><textarea id="text" required></textarea><button>Paraphrase</button></form><p id="error" role="alert"></p><h2>Result</h2><pre id="result" aria-live="polite"></pre></main>
<script>
const form=document.getElementById('form'), button=form.querySelector('button');
form.addEventListener('submit',async event=>{
event.preventDefault();button.disabled=true;document.getElementById('error').textContent='';
try {
const response=await fetch('/api/metaphrase',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:document.getElementById('text').value})});
const data=await response.json();
if(!response.ok)throw new Error(data.error?.message??'Please retry.');
document.getElementById('result').textContent=data.result.text;
}catch(error){document.getElementById('error').textContent=error.message;}
finally{button.disabled=false;}
});
</script></body></html>
The browser never receives METAPHRASE_API_KEY. It reads the result from data.result.text; errors come from data.error.message. provider_not_configured means the MetaPhrase operator must configure a provider, not that this proxy needs a different payload.