Showing posts with label Mangum. Show all posts
Showing posts with label Mangum. Show all posts

Sunday, 21 September 2025

Serve Your Frontend via the Backend with FastAPI (and ship it on AWS Lambda)

Standard

Let's start with an example for better understanding.

If your devices are allowed to talk only to your own backend (no third-party sites), the cleanest path is to serve the UI directly from your FastAPI app  i.e. HTML, CSS, JS, and images and expose JSON endpoints under the same domain. This post shows a production-practical pattern: a static, Bootstrap-styled UI (Login → Welcome → Weather with auto-refresh) fronted entirely by FastAPI, plus a quick path to deploy on AWS Lambda.

This article builds on an example project with pages /login, /welcome, /weather, health checks, and a weather API using OpenWeatherMap, already structured for Lambda.

Why “front via backend” (a.k.a. backend-served UI)?

  • Single domain: Avoids CORS headaches, cookie confusion, and device restrictions that block third-party websites.
  • Security & control: Gate all traffic through your API (auth, rate limiting, WAF/CDN).
  • Simplicity: One deployable artifact, one CDN/domain, one set of logs.
  • Edge caching: Cache static assets while keeping API dynamic.

Minimal project layout

fastAPIstaticpage/
├── main.py                 # FastAPI app
├── lambda_handler.py       # Mangum/handler for Lambda
├── requirements.txt
├── static/
│   ├── css/style.css
│   ├── js/login.js
│   ├── js/welcome.js
│   ├── js/weather.js
│   ├── login.html
│   ├── welcome.html
│   └── weather.html
└── (serverless.yml or template.yaml, deploy.sh)

The static directory holds your UI; FastAPI serves those files and exposes API routes like /api/login, /api/welcome, /api/weather.

FastAPI: serve pages + APIs from one app

1) Boot the app and mount static files

# main.py
import os, httpx
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles

OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY")

app = FastAPI(title="Frontend via Backend with FastAPI")

# Serve everything under /static (CSS/JS/Images/HTML)
app.mount("/static", StaticFiles(directory="static"), name="static")

# Optionally make pretty routes for pages:
@app.get("/", include_in_schema=False)
@app.get("/login", include_in_schema=False)
def login_page():
    return FileResponse("static/login.html")

@app.get("/welcome", include_in_schema=False)
def welcome_page():
    return FileResponse("static/welcome.html")

@app.get("/weather", include_in_schema=False)
def weather_page():
    return FileResponse("static/weather.html")

Tip: If you prefer templating (Jinja2) over plain HTML files, use from fastapi.templating import Jinja2Templates and render context from the server. For pure static HTML + fetch() calls, FileResponse is perfect.

2) JSON endpoints that the UI calls

@app.post("/api/login")
async def login(payload: dict):
    email = payload.get("email")
    password = payload.get("password")
    # Demo only: replace with proper auth in production
    if email == "admin" and password == "admin":
        return {"ok": True, "user": {"email": email}}
    raise HTTPException(status_code=401, detail="Invalid credentials")

@app.get("/api/welcome")
async def welcome():
    # In real apps, read user/session; here we return a demo message
    return {"message": "Welcome back, Admin!"}

@app.get("/api/weather")
async def weather(city: str = "Bengaluru", units: str = "metric"):
    if not OPENWEATHER_API_KEY:
        raise HTTPException(500, "OPENWEATHER_API_KEY missing")
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {"q": city, "appid": OPENWEATHER_API_KEY, "units": units}
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(url, params=params)
    if r.status_code != 200:
        raise HTTPException(r.status_code, "Weather API error")
    return r.json()

@app.get("/health", include_in_schema=False)
def health():
    return {"status": "ok"}

The pages: keep HTML static, fetch data with JS

static/login.html (snippet)

<form id="loginForm">
  <input name="email" placeholder="email" />
  <input name="password" type="password" placeholder="password" />
  <button type="submit">Sign in</button>
</form>
<script src="/static/js/login.js"></script>

static/js/login.js (snippet)

document.getElementById("loginForm").addEventListener("submit", async (e) => {
  e.preventDefault();
  const form = new FormData(e.target);
  const res = await fetch("/api/login", {
    method: "POST",
    headers: {"Content-Type":"application/json"},
    body: JSON.stringify({ email: form.get("email"), password: form.get("password") })
  });
  if (res.ok) location.href = "/welcome";
  else alert("Invalid credentials");
});

static/weather.html (snippet)

<div>
  <h2>Weather</h2>
  <select id="city">
    <option>Bengaluru</option><option>Mumbai</option><option>Delhi</option>
  </select>
  <pre id="result">Loading...</pre>
</div>
<script src="/static/js/weather.js"></script>

static/js/weather.js (snippet, 10s auto-refresh)

async function load() {
  const city = document.getElementById("city").value;
  const r = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
  document.getElementById("result").textContent = JSON.stringify(await r.json(), null, 2);
}
document.getElementById("city").addEventListener("change", load);
load();
setInterval(load, 10_000); // auto-refresh every 10s

The example app in the attached README uses the same flow: / (login) → /welcome/weather, with Bootstrap UI and a 10-second weather refresh.

Shipping it on AWS Lambda (two quick options)

You can deploy the exact same app to Lambda behind API Gateway.

Option A: SAM (recommended for many teams)

  1. Add template.yaml and run:
sam build
sam deploy --guided
  1. Point a domain (Route 53) + CloudFront if needed for caching static assets.
    (These steps mirror the attached project scaffolding.)

Option B: Serverless Framework

npm i -g serverless serverless-python-requirements
serverless deploy

Both approaches package your FastAPI app for Lambda. If you prefer a single entrypoint, use Mangum:

# lambda_handler.py
from mangum import Mangum
from main import app

handler = Mangum(app)

Pro tip: set appropriate cache headers for /static/* and no-cache for JSON endpoints.

Production hardening checklist

  • Auth: Replace demo creds with JWT/session, store secrets in AWS Secrets Manager.
  • HTTPS only: Enforce TLS; set Secure, HttpOnly, SameSite on cookies if used.
  • Headers: Add CSP, X-Frame-Options, Referrer-Policy, etc. via a middleware.
  • CORS: Usually unnecessary when UI and API share the same domain—keep it off by default.
  • Rate limits/WAF: Use API Gateway/WAF; some CDNs block requests lacking User-Agent.
  • Observability: Push logs/metrics to CloudWatch; add /health and structured logs.
  • Performance: Cache static assets at CloudFront; compress; fingerprint files (e.g., app.abc123.js).

Architecture Diagram


This diagram illustrates how a single-origin architecture works when serving both frontend (HTML, CSS, JS) and backend (API) traffic through FastAPI running on AWS Lambda.

Flow of Requests

1. User / Device

    • The client (e.g., browser, in-vehicle device, mobile app) makes a request to your app domain.

2. CloudFront

    • Acts as a Content Delivery Network (CDN) and TLS termination point.
    • Provides caching, DDoS protection, and performance optimization.
    • All requests are routed through CloudFront.

3. API Gateway

    • CloudFront forwards the request to Amazon API Gateway.
    • API Gateway handles routing, throttling, authentication (if configured), and request validation.
    • All paths (/, /login, /welcome, /weather, /api/...) pass through here.

4. Lambda (FastAPI)

    • API Gateway invokes the AWS Lambda function running FastAPI (using Mangum).
    • This single app serves:

  • Static content (HTML/CSS/JS bundled with the Lambda package or EFS)
  • API responses (login, weather, welcome, etc.)

Supporting Components

1. Local / EFS / In-package static

  • Your frontend files (e.g., login.html, weather.html, JS bundles) are either packaged inside the Lambda zip, stored in EFS, or mounted locally.
  • This allows the FastAPI app to return HTML/JS without needing a separate S3 bucket.

2. Observability & Secrets

  • CloudWatch Logs & Metrics capture all Lambda and API Gateway activity (for debugging, monitoring, and alerting).
  • Secrets Manager stores sensitive data (e.g., OpenWeatherMap API key, DB credentials). Lambda retrieves these securely at runtime.

Why This Architecture ?

  • One origin (no separate frontend on S3), meaning devices only talk to your backend domain.
  • No CORS needed because UI and API share the same domain.
  • Tight control over auth, caching, and delivery.
  • Ideal when working with restricted environments (e.g., in-vehicle browsers or IoT devices).

When this architecture shines (and is cost-efficient) ?

Devices must hit only your domain

  • In-vehicle browsers, kiosk/IVI, corporate-locked devices.
  • You serve HTML/CSS/JS and APIs from one origin → no CORS, simpler auth, tighter control.

Low-to-medium, spiky traffic (pay-per-use wins)

  • Nights/weekends idle, bursts during the day or at launches.
  • Lambda scales to zero; you don’t pay for idle EC2/ECS.

Small/medium static assets bundled or EFS-hosted

  • App shell HTML + a few JS/CSS files (tens of KBs → a few MBs).
  • CloudFront caches most hits; Lambda mostly executes on first cache miss.

Simple global delivery needs

  • CloudFront gives TLS, caching, DDoS mitigation, and global POPs with almost no ops.

Tight teams / fast iteration

  • One repo, one deployment path (SAM/Serverless).
  • Great for prototypes → pilot → production without re-architecting.

Traffic & cost heuristics (rules of thumb)

Use these to sanity-check costs; they’re order-of-magnitude, excluding data transfer:

Lambda is cheapest when:

  • Average load is bursty and < a few million requests/month, and
  • Per-request work is modest (sub-second, 128–512 MB memory), and
  • Static assets are cache-friendly (CloudFront hit ratio high).

Rough mental math (how to approximate)

  • Per-request Lambda cost ≈ (memory GB) × (duration sec) × (price per GB-s) + (request charge).
  • Example shape (not exact pricing): at 256 MB and 200 ms, compute cost per 100k requests is typically pennies to low dollars; the bigger bill tends to be egress/data transfer if your assets are large.
  • CloudFront greatly reduces Lambda invocations for static paths (high cache hit ratio → far fewer Lambda runs).

If your bill is mostly data (images, big JS bundles, downloads), move those to S3 + CloudFront (dual-origin). It’s almost always cheaper for heavy static.

Perfect fits (based on real-world patterns)

  • In-vehicle Apps with Web view : UI must come from your backend only; traffic is intermittent; pages are light; auth and policies live at the edge/API Gateway.
  • Internal tools, admin consoles, partner portals with uneven usage.
  • Geo-gated or compliance-gated UIs where a single origin simplifies policy.
  • Early-stage products and pilots where you want minimal ops and fast changes.

When to switch (or start with a dual-origin) ?

  • Front-heavy sites (lots of images/video, large JS bundles)
Use S3 + CloudFront for /static/* and keep Lambda for /api/*.
Same domain via CloudFront behaviors → still no CORS.

  • High, steady traffic (always busy)
If you’re sustaining high RPS all day, Fargate/ECS/EC2 behind ALB can beat Lambda on cost and cold-start latency.

  • Very low latency or long-lived connections
Ultra-low p95 targets, or WebSockets with heavy fan-out → consider ECS/EKS or API Gateway WebSockets with tailored design.

  • Heavy CPU/GPU per request (ML inference, large PDFs, video processing)

Dedicated containers/instances (ECS/EKS/EC2) with right sizing are usually cheaper and faster.

Simple decision tree

Do you need single origin + locked-down devices?
    Yes → Single-origin Lambda is great.

Are your static assets > a few MB and dominate traffic?
    Yes → Dual-origin (S3 for static + Lambda for API).

Is traffic high and steady (e.g., >5–10M req/mo with sub-second work)?
    Consider ECS/Fargate for cost predictability.

Do you need near-zero cold-start latency?
    Prefer containers or keep Lambda warm (provisioned concurrency → raises cost).

Cost-saving tips (keep Lambda, cut the bill)

  • Cache hard at CloudFront: long TTLs for /static/*, hashed filenames; no-cache for /api/*.
  • Slim assets: compress, tree-shake, code-split, use HTTP/2.
  • Right-size Lambda memory: test 128/256/512 MB; pick the best $/latency.
  • Warm paths (if needed): provisioned concurrency only on critical API stages/times.
  • Move heavy static to S3 while keeping single domain via CloudFront behaviors.

Bottom line

  • If your devices can only call your backend, traffic is bursty/medium, and your frontend is lightweight, this Lambda + API Gateway + CloudFront single-origin setup is both operationally simple and cost-efficient.
  • As static volume or steady traffic grows, go dual-origin (S3 + Lambda) first; if traffic becomes large and constant or latency targets tighten, move APIs to containers.

Bibliography