React and Next.js integration
Keep the MetaPhrase key in an App Router backend route and call that route from React.
Create a TypeScript App Router project, then add the following files. Requires Node.js 22+.
npx [email protected] my-writing-app --ts --app --no-src-dir --use-npm --yes
cd my-writing-app
Set METAPHRASE_API_KEY in .env.local on the server, without a NEXT_PUBLIC_ prefix. Do not commit it. Optional METAPHRASE_BASE_URL overrides the API origin.
Save as app/api/metaphrase/route.ts:
export const runtime='nodejs';
let windowStart=0, accepted=0;
export async function POST(request:Request){
const now=Date.now();if(now-windowStart>=60000){windowStart=now;accepted=0;}
if(++accepted>30)return Response.json({error:{message:'Please retry in a minute.'}},{status:429,headers:{'Retry-After':'60'}});
const origin=request.headers.get('origin');
if(origin && origin!==new URL(request.url).origin)return Response.json({error:{message:'Origin not allowed'}},{status:403});
const key=process.env.METAPHRASE_API_KEY;
if(!key)return Response.json({error:{message:'The backend key is not configured.'}},{status:503});
if(Number(request.headers.get('content-length'))>120000)return Response.json({error:{message:'Request too large'}},{status:413});
const reader=request.body?.getReader();if(!reader)return Response.json({error:{message:'Missing JSON body'}},{status:400});
const chunks:Uint8Array[]=[];let size=0;
try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>120000){await reader.cancel();return Response.json({error:{message:'Request too large'}},{status:413});}chunks.push(value);}}finally{reader.releaseLock();}
let payload:unknown;
try{payload=JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{return Response.json({error:{message:'Invalid JSON'}},{status:400});}
const text=payload && typeof payload==='object' && 'text' in payload?payload.text:undefined;
if(typeof text!=='string'||!text.trim()||[...text].length>20000)return Response.json({error:{message:'Enter text within the character limit.'}},{status:400});
try{
const response=await fetch((process.env.METAPHRASE_BASE_URL??'https://metaphrase.msalman.com').replace(/\/$/,'')+'/api/v1/paraphrase',{method:'POST',headers:{Authorization:'Bearer '+key,'Content-Type':'application/json'},body:JSON.stringify({text,mode:'standard',stream:false}),signal:AbortSignal.timeout(250000),cache:'no-store'});
const headers:Record<string,string>={'Cache-Control':'no-store'};if(response.headers.has('retry-after'))headers['Retry-After']=response.headers.get('retry-after')!;
return Response.json(await response.json(),{status:response.status,headers});
}catch{return Response.json({error:{message:'The writing service is temporarily unavailable.'}},{status:502});}
}
Save as app/page.tsx:
'use client';
import {useState,type FormEvent} from 'react';
export default function Page(){
const [text,setText]=useState(''),[output,setOutput]=useState(''),[error,setError]=useState(''),[busy,setBusy]=useState(false);
async function submit(event:FormEvent){event.preventDefault();setBusy(true);setError('');
try{const response=await fetch('/api/metaphrase',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text})});const value=await response.json();if(!response.ok)throw new Error(value.error?.message??'Please retry.');setOutput(value.result.text);}
catch(error){setError((error as Error).message);}finally{setBusy(false);}}
return <main style={{maxWidth:760,margin:'50px auto',padding:20,fontFamily:'system-ui'}}><h1>My paraphraser</h1><form onSubmit={submit}><label htmlFor="text">Your text</label><textarea id="text" value={text} onChange={event=>setText(event.target.value)} required style={{display:'block',width:'100%',height:180,padding:12}}/><button disabled={busy||!text.trim()}>{busy?'Writing…':'Paraphrase'}</button></form>{error&&<p role="alert">{error}</p>}<h2>Result</h2><pre style={{whiteSpace:'pre-wrap'}} aria-live="polite">{output}</pre></main>;
}
Run npm run dev and open the local URL. The conservative 30/minute demo limiter is shared by users in one process. For a public multi-instance application, use your app's authentication and a Redis-backed limiter. The MetaPhrase secret stays in the route handler; the React component receives only the response.
For an existing React/Vite app, use the same page component and the standalone Node proxy. Configure your dev server to forward /api/metaphrase to that backend. Do not move the secret into a Vite environment variable.