Browse documentation
METAPHRASE / RECIPESREST API v1
Open in AI
Open in ChatGPT Open in Claude
Markdown

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.