Python / FastAPI
In this chapter, you’ll integrate Oicana into a Python web service using FastAPI. FastAPI is a modern, high-performance web framework for building APIs with Python based on standard Python type hints. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Let’s start with a fresh FastAPI project. First, create a new directory for your project, then initialize it and install FastAPI with uv init && uv add "fastapi[standard]". Replace the content of main.py with the following basic FastAPI application:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")async def root(): return {"message": "Hello World"}You can test it by running uv run fastapi dev main.py and navigating to http://localhost:8000 in your browser.
New service endpoint
Section titled “New service endpoint”We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
-
Create a new directory in the Python project called
templatesand copyexample-0.1.0.zipinto that directory. -
Add the
oicanaPyPI package as a dependency withuv add oicana. -
Update
main.pyto load the template at startup and add a compile endpoint:main.py from contextlib import asynccontextmanagerfrom pathlib import Pathfrom fastapi import FastAPIfrom fastapi.responses import Responsefrom oicana import Template, CompilationModetemplate: Template@asynccontextmanagerasync def lifespan(app: FastAPI):global templatetemplate_path = Path("templates/example-0.1.0.zip")template_bytes = template_path.read_bytes()# Template registration uses development mode by defaulttemplate = Template(template_bytes)yieldapp = FastAPI(lifespan=lifespan)@app.post("/compile")def compile_template():pdf = template.export_pdf(mode=CompilationMode.DEVELOPMENT)return Response(content=pdf,media_type="application/pdf",headers={"Content-Disposition": "attachment; filename=example.pdf"},)This code loads the template once at application startup using FastAPI’s lifespan context manager. The
/compileendpoint compiles the template and returns the PDF file. We explicitly pass the compilation mode withtemplate.export_pdf(mode=CompilationMode.DEVELOPMENT)so the template uses the development value you defined for theinfoinput ({ "name": "Chuck Norris" }). In a follow-up step, we will set an input value instead.
After restarting the service, you can test the endpoint with curl:
curl -X POST http://localhost:8000/compile --output example.pdfThe generated example.pdf file should contain your template with the development value.
You can also explore the automatically generated API documentation by navigating to http://localhost:8000/docs in your browser. FastAPI provides interactive API documentation out of the box.
About performance
Section titled “About performance”The PDF generation should not take longer than a couple of milliseconds.
Note that the endpoint uses plain def instead of async def. PDF compilation is synchronous native code, so it would block the event loop in an async def endpoint. FastAPI runs def endpoints in a thread pool, and Oicana releases the GIL during compilation, so the service stays responsive.
Compilations of the same Template instance serialize internally. For more concurrency under heavy load, create multiple Template instances from the same template file or deploy multiple worker processes using Gunicorn or similar ASGI servers.
Repeated compilations are fast because Typst memoizes its work in a global cache. The cache management guide explains how that cache is evicted and when it pays off to tune it.
Passing inputs from Python
Section titled “Passing inputs from Python”Our compile_template function is currently calling template.export_pdf() with development mode. Now we’ll provide explicit input values and switch to production mode:
import json
# previous code...
@app.post("/compile")def compile_template(): pdf = template.export_pdf( json_inputs={"info": json.dumps({"name": "Baby Yoda"})} )
return Response( content=pdf, media_type="application/pdf", headers={ "Content-Disposition": "attachment; filename=example.pdf" }, )With this change, CompilationMode is no longer used and can be dropped from the oicana import.
Your explicit input value takes precedence over the development value in either mode, so it is what changes the output here. Notice that we removed the explicit mode=CompilationMode.DEVELOPMENT parameter. The export_pdf() method defaults to CompilationMode.PRODUCTION when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles none values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the open source FastAPI example project on GitHub for a more complete showcase of the Oicana Python integration, including blob inputs, error handling, and request models.
For inputs other than JSON, see Template inputs, which documents blob inputs with examples for every integration.
Handling compilation errors
Section titled “Handling compilation errors”A missing required input or an input that fails schema validation makes the compilation fail. Wrap the call so you can log the details and answer with something useful:
import jsonimport loggingfrom contextlib import asynccontextmanagerfrom pathlib import Path
from fastapi import FastAPI, HTTPExceptionfrom fastapi.responses import Responsefrom oicana import Template
# previous code...
@app.post("/compile")def compile_template(): try: pdf = template.export_pdf( json_inputs={"info": json.dumps({"name": "Baby Yoda"})} ) except Exception as error: logging.exception("Failed to compile template") raise HTTPException( status_code=500, detail="Failed to generate the document" ) from error
return Response( content=pdf, media_type="application/pdf", headers={ "Content-Disposition": "attachment; filename=example.pdf" }, )Note the two changes to the imports: logging, and HTTPException alongside FastAPI.
Complete code at the end of this chapter
import jsonimport loggingfrom contextlib import asynccontextmanagerfrom pathlib import Path
from fastapi import FastAPI, HTTPExceptionfrom fastapi.responses import Responsefrom oicana import Template
template: Template
@asynccontextmanagerasync def lifespan(app: FastAPI): global template
template_path = Path("templates/example-0.1.0.zip") template_bytes = template_path.read_bytes() template = Template(template_bytes) yield
app = FastAPI(lifespan=lifespan)
@app.post("/compile")def compile_template(): try: pdf = template.export_pdf( json_inputs={"info": json.dumps({"name": "Baby Yoda"})} ) except Exception as error: logging.exception("Failed to compile template") raise HTTPException( status_code=500, detail="Failed to generate the document" ) from error
return Response( content=pdf, media_type="application/pdf", headers={ "Content-Disposition": "attachment; filename=example.pdf" }, )