Generate PDF responses with FastAPI, Flask, and Django¶
Fullbleed returns PDF bytes from Python data and HTML/CSS. Pass those bytes to your framework's response object to serve an invoice, report, or statement. The examples below generate the same one-page invoice and send it as a download.
Get the complete examples Try a PDF in your browser
Set up the examples¶
Use a Python 3.10–3.14 virtual environment, then get the example source:
git clone https://github.com/fullbleed-engine/fullbleed-official.git
cd fullbleed-official/examples/web_frameworks
Choose one framework below. Each requirement file installs that framework;
fullbleed remains a separate package. The examples are checked with Fullbleed
2.4.0, FastAPI 0.142.2, Flask 3.1.3, and Django 5.2.17.
Each app serves this fictional record:
Open that URL after starting the app. The browser downloads invoice.pdf, which
contains three line items and a total of USD 1,870.00. An unknown invoice ID
returns HTTP 404. Stop the server before trying another framework on the same port.
FastAPI¶
python -m pip install fullbleed -r requirements-fastapi.txt
python -m uvicorn fastapi_app:app --host 127.0.0.1 --port 8000
The route in fastapi_app.py uses the shared invoice.py renderer:
from fastapi import FastAPI, HTTPException, Response
from invoice import PDF_HEADERS, load_invoice, render_invoice
class PDFResponse(Response):
media_type = "application/pdf"
app = FastAPI(title="Fullbleed invoice example")
@app.get("/invoices/{invoice_id}.pdf", response_class=PDFResponse)
def invoice_pdf(invoice_id: str) -> PDFResponse:
invoice = load_invoice(invoice_id)
if invoice is None:
raise HTTPException(status_code=404, detail="Invoice not found")
return PDFResponse(render_invoice(invoice), headers=PDF_HEADERS)
The custom response class advertises application/pdf in OpenAPI. You can also
try the route at http://127.0.0.1:8000/docs. See
FastAPI's response documentation
for how explicit response classes affect the generated API schema.
The route uses a normal def because rendering is synchronous. FastAPI runs
these route functions in its thread pool. Calling a synchronous renderer directly
inside async def would still occupy that event-loop thread until rendering
finishes. See FastAPI's concurrency documentation.
Flask¶
python -m pip install fullbleed -r requirements-flask.txt
python -m flask --app flask_app run --host 127.0.0.1 --port 8000
flask_app.py returns a regular Flask response:
from flask import Flask, Response, abort
from invoice import PDF_HEADERS, load_invoice, render_invoice
app = Flask(__name__)
@app.get("/invoices/<invoice_id>.pdf")
def invoice_pdf(invoice_id: str) -> Response:
invoice = load_invoice(invoice_id)
if invoice is None:
abort(404)
return Response(
render_invoice(invoice), mimetype="application/pdf", headers=PDF_HEADERS
)
See Flask's Response API for response bodies, media types, and headers.
Django¶
python -m pip install fullbleed -r requirements-django.txt
python django_app.py runserver 127.0.0.1:8000 --noreload
The complete Django example includes minimal local settings. In an existing project, keep your project's settings and adapt this view and URL pattern:
from django.http import Http404, HttpResponse
from django.urls import path
from django.views.decorators.http import require_safe
from invoice import PDF_HEADERS, load_invoice, render_invoice
@require_safe
def invoice_pdf(request, invoice_id: str) -> HttpResponse:
invoice = load_invoice(invoice_id)
if invoice is None:
raise Http404("Invoice not found")
return HttpResponse(
render_invoice(invoice), content_type="application/pdf", headers=PDF_HEADERS
)
urlpatterns = [path("invoices/<str:invoice_id>.pdf", invoice_pdf)]
Adjust the invoice import to its location in your Django project. See
Django's HttpResponse documentation
for response construction and headers.
The shared document renderer¶
invoice.py
contains the fictional record lookup, HTML/CSS template, and render_invoice().
It calculates line totals with Decimal, escapes text before inserting it into
HTML, and embeds the Inter font shipped with Fullbleed. A new PdfEngine renders
each request into memory.
The response headers are shared too:
PDF_HEADERS = {
"Content-Disposition": 'attachment; filename="invoice.pdf"',
"Cache-Control": "private, no-store",
}
The download filename is fixed. To display the PDF in the browser instead,
change attachment to inline. The response carries binary PDF bytes directly;
it does not need JSON or base64 encoding, and requests do not share output files.
Replace load_invoice() with your application's authorized record lookup. It
returns an invoice number, customer, date strings, and items with descriptions,
integer quantities, and unit-price strings. Keep access checks before rendering.
This sample omits tax and discounts.
Verify and deploy your workflow¶
From the repository root, with Fullbleed installed:
python -m pip install -r examples/web_frameworks/requirements-check.txt
python examples/web_frameworks/check_examples.py --out target/web-framework-check
The check exercises each framework's test client, saves its PDF response, and
verifies the expected text and total, page count, embedded font, HTTP headers,
404 behavior, and repeated output bytes. It also emits a PNG preview and
verification.json. Review the preview after changing the document's layout.
The launch commands above run local development servers. Use your framework's deployment setup for a public app. For large jobs, render in your job queue and store the finished PDF for download. The variable-data guide covers compiled document families when many records share a template.