Typst Webservice compiles Typst templates into PDFs given a JSON input. Templates, assets, and fonts are preloaded from an on-disk directory. It ships as both a library (PdfContext::render, PdfContext::render_batch) and an optional Axum-based HTTP server.
GET /render-pdf/{template}/{file_name}renders a single template into PDF.POST /render-pdf/batchrenders multiple templates and returns a streaming ZIP archive.- Streaming ZIP writer keeps memory usage predictable for large batches.
- Detailed error responses include unique reference IDs for troubleshooting.
- Structured logging powered by
tracing.
-
server(default): enables the Axum-based HTTP server, thetypst-webservicebinary, and thehandlers/ZipResponsetypes. Disable withdefault-features = falseto use the library without any HTTP dependencies:[dependencies] typst-webservice = { version = "0.5", default-features = false }
The library-only build still exposes
PdfContext::render(single PDF) andPdfContext::render_batch(ZIP of PDFs) alongside the lower-levelrender_batch_to_writerfor streaming into a user-providedAsyncWrite.
cargo runBy default the server loads templates from the assets/ directory in the project root and binds to 127.0.0.1:8080.
You can point the service at a different assets directory using either a command-line argument or an environment variable:
# Command-line override
cargo run -- ./my-templates
# Environment variable override
TWS_DIR=./my-templates cargo runThe command-line argument takes precedence; both fall back to assets/ when unset.
With default-features = false the crate has no HTTP dependencies and exposes just the rendering pipeline.
A PdfContext holds all Typst sources, fonts, and binary assets in memory. Build one from a directory or from in-memory tuples:
use std::sync::Arc;
use typst_webservice::PdfContext;
// From a directory on disk.
let context = PdfContext::from_directory("./assets")?;
// Or from in-memory files (e.g. embedded via `include_bytes!`).
let context = PdfContext::from_assets(&[
("example.typ", include_bytes!("../assets/example.typ")),
("Bagnard.otf", include_bytes!("../assets/Bagnard.otf")),
])?;
// Share the context between render calls.
let context = Arc::new(context);PdfContext::render takes the template file name, a serde_json::Value payload (exposed inside the template as input.json), and returns the PDF bytes:
use std::sync::Arc;
use typst_webservice::PdfContext;
let context = Arc::new(PdfContext::from_directory("./assets")?);
let pdf_bytes = PdfContext::render(
context,
"example.typ".to_string(),
serde_json::json!({
"name": "World",
"list": ["Memory Safety", "Open Source", "World Peace"],
}),
)?;
std::fs::write("out.pdf", pdf_bytes)?;render runs a synchronous Typst compile; call it from a blocking context (or wrap it in tokio::task::spawn_blocking when running inside an async runtime).
PdfContext::render_batch renders many templates in parallel and returns the resulting ZIP as Vec<u8>:
use std::sync::Arc;
use typst_webservice::{BatchRenderRequest, PdfContext};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let context = Arc::new(PdfContext::from_directory("./assets")?);
let requests = vec![
BatchRenderRequest {
template: "example.typ".to_string(),
file_name: "first.pdf".to_string(),
input: serde_json::json!({ "name": "One", "list": ["Item"] }),
},
BatchRenderRequest {
template: "example.typ".to_string(),
file_name: "second.pdf".to_string(),
input: serde_json::json!({ "name": "Two", "list": ["Item"] }),
},
];
let zip_bytes = PdfContext::render_batch(context, requests).await?;
std::fs::write("out.zip", zip_bytes)?;
Ok(())
}If any request references a template that is not loaded in the context, render_batch returns AppError::MainSourceNotFound before rendering starts. The call requires a Tokio runtime because rendering happens on a spawn_blocking pool and PDFs are fed through an async ZIP writer.
For large batches where you don't want to buffer the whole archive in memory, use render_batch_to_writer with any tokio::io::AsyncWrite:
use std::sync::Arc;
use tokio::fs::File;
use typst_webservice::{BatchRenderRequest, PdfContext};
use typst_webservice::zip::ZipResponseWriter;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let context = Arc::new(PdfContext::from_directory("./assets")?);
let requests: Vec<BatchRenderRequest> = /* ... */ vec![];
let file = File::create("out.zip").await?;
let writer = ZipResponseWriter::new(file);
PdfContext::render_batch_to_writer(context, requests, writer).await?;
Ok(())
}render_batch_to_writer finishes (and shuts down) the writer before returning it, so the archive is complete as soon as the call resolves.
cargo testIntegration tests exercise both single and batch rendering flows using fixtures from the assets/ directory.