FormaTeX

\usepackage{rust}

Rust LaTeX to PDF API

Compile LaTeX to PDF from Rust using the FormaTeX REST API. Async reqwest + tokio — no TeX Live installation, no binary to bundle.

Also available in:Python·Node.js·Go·PHP·Ruby·Rust·cURL|Full API reference

\section{Why Rust}

Why Rust + FormaTeX

Zero-overhead async

reqwest's async client maps naturally onto Tokio's runtime. Compile multiple documents concurrently with no extra complexity.

Lean Docker images

Your Rust binary stays small — no TeX Live, no apt install texlive-full. FormaTeX handles the compilation environment entirely.

Type-safe error handling

Rust's Result type pairs well with the FormaTeX API. Map HTTP errors to domain errors at the type level — no runtime surprises.

\section{Dependencies}

Cargo.toml dependencies

Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
serde = { version = "1", features = ["derive"] }

# For Axum integration
axum = "0.7"
bytes = "1"

\section{Quick Start}

Quick start with reqwest

The example below compiles a minimal LaTeX document using reqwest and writes the resulting PDF to disk. Read your API key from the environment with std::env::var("FORMATEX_API_KEY").

src/main.rs
use reqwest::Client;
use serde_json::json;
use std::fs;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = std::env::var("FORMATEX_API_KEY")?;

    let latex = r#"\documentclass{article}
\usepackage{amsmath}
\begin{document}
Hello from Rust! $E = mc^2$
\end{document}"#;

    let client = Client::new();
    let response = client
        .post("https://api.formatex.io/v1/compile/sync")
        .bearer_auth(&api_key)
        .json(&json!({ "source": latex, "engine": "pdflatex" }))
        .send()
        .await?;

    if !response.status().is_success() {
        let body = response.text().await?;
        return Err(format!("API error: {body}").into());
    }

    let pdf = response.bytes().await?;
    fs::write("output.pdf", &pdf)?;
    println!("PDF saved to output.pdf");
    Ok(())
}

\section{Axum Handler}

Axum handler

Register a shared AppState with your Axum router and mount compile_handler at any route. The handler accepts a JSON body with source and optional engine, then streams the PDF bytes back directly to the client.

src/handlers/compile.rs
use axum::{
    body::Bytes,
    extract::State,
    http::{HeaderMap, StatusCode},
    response::{IntoResponse, Response},
    Json,
};
use reqwest::Client;
use serde::Deserialize;

#[derive(Clone)]
pub struct AppState {
    pub http:    Client,
    pub api_key: String,
}

#[derive(Deserialize)]
pub struct CompileRequest {
    pub source: String,
    #[serde(default = "default_engine")]
    pub engine: String,
}

fn default_engine() -> String {
    "pdflatex".to_string()
}

pub async fn compile_handler(
    State(state): State<AppState>,
    Json(body):   Json<CompileRequest>,
) -> Result<Response, (StatusCode, String)> {
    let res = state
        .http
        .post("https://api.formatex.io/v1/compile/sync")
        .bearer_auth(&state.api_key)
        .json(&serde_json::json!({
            "source": body.source,
            "engine": body.engine,
        }))
        .send()
        .await
        .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;

    if !res.status().is_success() {
        let msg = res.text().await.unwrap_or_default();
        return Err((StatusCode::UNPROCESSABLE_ENTITY, msg));
    }

    let pdf: Bytes = res
        .bytes()
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    let mut headers = HeaderMap::new();
    headers.insert("Content-Type", "application/pdf".parse().unwrap());
    headers.insert(
        "Content-Disposition",
        "attachment; filename=output.pdf".parse().unwrap(),
    );

    Ok((headers, pdf).into_response())
}

\section{Authentication}

Environment variable setup

Store your API key in the environment — never hardcode it in source files. Read it at startup and fail fast if it is missing:

// Fail fast at startup
let api_key = std::env::var("FORMATEX_API_KEY").expect("FORMATEX_API_KEY not set");
  • Generate API keys from your dashboard after signing up.
  • Use a .env file with the dotenvy crate during local development.
  • All requests must include Authorization: Bearer <key>.

\section{Reference}

API reference

FieldTypeRequiredDescription
sourcestringYesFull LaTeX source code as a string
enginestringNoEngine to use. Default: "pdflatex". Options: pdflatex, xelatex, lualatex, latexmk

POST https://api.formatex.io/v1/compile/syncReturns raw PDF bytes on success (200), or JSON error on failure (422/401).

\section{Error Handling}

Error handling

Check response.status().is_success() before reading the body. Map HTTP errors to your own error type for clean propagation:

  • 200 — raw PDF bytes. Read with response.bytes().await?.
  • 422 — LaTeX compilation error. JSON body contains an error field with the TeX log excerpt.
  • 401 — invalid or missing API key.

\end{rust}

Start compiling LaTeX from Rust

Get your API key and have PDF output from Rust in minutes — no TeX Live required.

One quick thing

We track anonymous usage — page views, feature usage, compilation events — to understand what works and what doesn't. No ads, no personal data, no third-party sharing.

Cookie policy