# Django and FastAPI backends

Call MetaPhrase from a Python server and expose only your own proxy route to the browser.

Set `METAPHRASE_API_KEY` in the backend environment. Both examples use Python's standard-library HTTP client for the MetaPhrase call, preserve error JSON, and never render the key. Use the [plain HTML page](https://metaphrase.msalman.com/docs/recipe-html.md) as `index.html`; it already posts to `/api/metaphrase` without a key.

## FastAPI

Create a virtual environment, install the framework, and save the complete server below as `app.py` alongside `index.html`.

```bash
python3 -m venv .venv
. .venv/bin/activate
python -m pip install fastapi uvicorn
uvicorn app:app --host 127.0.0.1 --port 4000
```

```python
import asyncio
import json
import os
import threading
import time
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from fastapi import FastAPI, Request as WebRequest
from fastapi.responses import FileResponse, JSONResponse

app = FastAPI()
api_key = os.environ['METAPHRASE_API_KEY']
api_origin = os.environ.get('METAPHRASE_BASE_URL', 'https://metaphrase.msalman.com').rstrip('/')
public_origin = os.environ.get('PUBLIC_ORIGIN', 'http://127.0.0.1:4000')
lock = threading.Lock()
window = {'start': 0.0, 'count': 0}

def upstream(text):
    request = Request(api_origin + '/api/v1/paraphrase',
        data=json.dumps({'text': text, 'mode': 'standard', 'stream': False}).encode(),
        headers={'Authorization': 'Bearer ' + api_key, 'Content-Type': 'application/json'}, method='POST')
    try:
        with urlopen(request, timeout=250) as response:
            return response.status, json.load(response)
    except HTTPError as error:
        return error.code, json.loads(error.read())
    except (URLError, TimeoutError):
        return 502, {'error': {'message': 'The writing service could not be reached.'}}

@app.get('/')
def index():
    return FileResponse(Path(__file__).with_name('index.html'))

@app.post('/api/metaphrase')
async def paraphrase(request: WebRequest):
    if request.headers.get('origin') not in (None, public_origin):
        return JSONResponse({'error': {'message': 'Origin not allowed'}}, status_code=403)
    with lock:
        now = time.monotonic()
        if now - window['start'] >= 60:
            window.update(start=now, count=0)
        window['count'] += 1
        if window['count'] > 30:
            return JSONResponse({'error': {'message': 'Please wait a minute.'}}, status_code=429, headers={'Retry-After':'60'})
    raw = bytearray()
    async for chunk in request.stream():
        raw.extend(chunk)
        if len(raw) > 120000:
            return JSONResponse({'error': {'message': 'Request too large'}}, status_code=413)
    try:
        value = json.loads(raw)
        text = value.get('text') if isinstance(value, dict) else None
        if not isinstance(text, str) or not text.strip() or len(text) > 20000:
            raise ValueError()
    except (ValueError, UnicodeError):
        return JSONResponse({'error': {'message': 'Enter valid text within the character limit.'}}, status_code=400)
    status, result = await asyncio.to_thread(upstream, text)
    return JSONResponse(result, status_code=status, headers={'Cache-Control':'no-store'})
```

Open `http://127.0.0.1:4000`. The demo guard is a shared 30/minute process limit. Use your production application's authentication and a shared limiter when scaling to multiple workers.

## Django

For a fresh project:

```bash
python -m pip install Django
mkdir django-writer
cd django-writer
django-admin startproject writer .
```

Copy `index.html` from the HTML recipe beside `manage.py`. In that HTML page's fetch headers add `'X-CSRFToken': document.cookie.split('; ').find(item=>item.startsWith('csrftoken='))?.split('=')[1] ?? ''`. Django issues this cookie on the initial GET below. Do not disable CSRF middleware.

Save `writer/views.py`:

```python
import json
import os
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from django.core.cache import cache
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST

@ensure_csrf_cookie
def index(request):
    return HttpResponse((Path(__file__).resolve().parent.parent / 'index.html').read_text(), content_type='text/html')

@require_POST
def paraphrase(request):
    cache.add('metaphrase_demo_window', 0, 60)
    if cache.incr('metaphrase_demo_window') > 30:
        response = JsonResponse({'error': {'message': 'Please wait a minute.'}}, status=429)
        response['Retry-After'] = '60'
        return response
    if int(request.META.get('CONTENT_LENGTH') or 0) > 120000 or len(request.body) > 120000:
        return JsonResponse({'error': {'message': 'Request too large'}}, status=413)
    try:
        value = json.loads(request.body)
        text = value.get('text') if isinstance(value, dict) else None
        if not isinstance(text, str) or not text.strip() or len(text) > 20000:
            raise ValueError()
    except (ValueError, UnicodeError):
        return JsonResponse({'error': {'message': 'Enter valid text within the character limit.'}}, status=400)
    key = os.environ.get('METAPHRASE_API_KEY')
    if not key:
        return JsonResponse({'error': {'message': 'Backend key is not configured.'}}, status=503)
    base = os.environ.get('METAPHRASE_BASE_URL', 'https://metaphrase.msalman.com').rstrip('/')
    upstream = Request(base + '/api/v1/paraphrase', data=json.dumps({'text':text,'mode':'standard','stream':False}).encode(), headers={'Authorization':'Bearer '+key,'Content-Type':'application/json'}, method='POST')
    try:
        with urlopen(upstream, timeout=250) as response:
            result, status = json.load(response), response.status
    except HTTPError as error:
        result, status = json.loads(error.read()), error.code
    except (URLError, TimeoutError):
        result, status = {'error': {'message': 'The writing service could not be reached.'}}, 502
    response = JsonResponse(result, status=status)
    response['Cache-Control'] = 'no-store'
    return response
```

Save `writer/urls.py`:

```python
from django.urls import path
from . import views
urlpatterns = [path('', views.index), path('api/metaphrase', views.paraphrase)]
```

Run `python manage.py migrate`, then `python manage.py runserver 127.0.0.1:4000`. Use your application's authentication and shared cache/rate controls for public production access. Both examples keep the MetaPhrase secret strictly in the Python process.
