Go / net/http
In this chapter, you’ll integrate Oicana into a Go web service using net/http from the standard library. Since Go 1.22, its router matches HTTP methods and path patterns, so many Go services need no web framework at all. 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 Go module. Create a new directory for your project (separate from your template directory) and initialize the module:
mkdir my-pdf-servicecd my-pdf-servicego mod init example.com/my-pdf-serviceCreate a main.go file with the following basic web service:
package main
import ( "fmt" "log" "net/http")
func main() { http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello World") })
log.Fatal(http.ListenAndServe(":8080", nil))}You can test it by running go run . and navigating to http://localhost:8080 in your browser. You should see “Hello World”.
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 Go project called
templatesand copyexample-0.1.0.zipinto that directory. -
Add the Oicana module as a dependency and fetch the native library for your platform:
Terminal window go get go.oicana.com/oicanaexport CGO_LDFLAGS="$(go run go.oicana.com/oicana/cmd/oicana-install)" -
Update
main.goto load the template at startup and replace theGET /route with a compile endpoint:main.go package mainimport ("log""net/http""os"oicana "go.oicana.com/oicana")func main() {templateBytes, err := os.ReadFile("templates/example-0.1.0.zip")if err != nil {log.Fatalf("Failed to load the template file: %v", err)}// Creating the template runs a warm-up compilation in development mode,// which uses the development value of the input.template, err := oicana.NewTemplate(templateBytes)if err != nil {log.Fatalf("Failed to create the template: %v", err)}defer template.Close()http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) {pdf, err := template.ExportPdf(oicana.Inputs{}, oicana.WithMode(oicana.Development))if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.Header().Set("Content-Type", "application/pdf")w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`)w.Write(pdf)})log.Fatal(http.ListenAndServe(":8080", nil))}Run
go mod tidyto record the Oicana module as a direct dependency now thatmain.goimports it.This code loads the template once at application startup. The
/compileendpoint compiles the template and returns the PDF file. We pass no inputs and explicitly set the compilation mode withoicana.WithMode(oicana.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 with go run ., you can test the endpoint with curl:
curl -X POST http://localhost:8080/compile --output example.pdfThe generated example.pdf file should contain your template with the development value.
About performance
Section titled “About performance”The PDF generation should not take longer than a couple of milliseconds.
net/http serves every request in its own goroutine, and a Template is safe for concurrent use, so a single instance can serve all requests. Compilations of the same Template instance serialize internally. For more concurrency under heavy load, create multiple Template instances from the same template file.
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 Go
Section titled “Passing inputs from Go”Our compile endpoint is currently calling template.ExportPdf() with development mode. Now we’ll provide explicit input values and switch to production mode. Replace the POST /compile handler in main.go with this version:
http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) { pdf, err := template.ExportPdf(oicana.Inputs{ JSON: map[string]any{"info": map[string]any{"name": "Baby Yoda"}}, }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`) w.Write(pdf)})JSON input values are encoded with json.Marshal, so you can also pass your own structs. A Go string becomes a JSON string, so pass a json.RawMessage for JSON you already have.
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 oicana.WithMode(oicana.Development) option. ExportPdf() defaults to oicana.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 Go example project on GitHub for a more complete showcase of the Oicana Go integration, including blob inputs, error handling, and request validation.
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. The tutorial’s example template declares no JSON schema, so only the first case can happen here. Our endpoint currently sends the error message to the client, which can leak details of your template. Replace the handler again to log the error and answer with a generic message:
http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) { pdf, err := template.ExportPdf(oicana.Inputs{ JSON: map[string]any{"info": map[string]any{"name": "Baby Yoda"}}, }) if err != nil { log.Printf("Failed to compile template: %v", err) http.Error(w, "Failed to generate the document", http.StatusInternalServerError) return }
w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`) w.Write(pdf)})Every error returned by the Oicana module is an *oicana.Error. Check its kind with errors.Is. oicana.ErrInvalidInput covers inputs that fail schema validation or that the template does not declare, so you can answer those with HTTP 400. A missing required input fails the compilation itself and is an oicana.ErrCompilation.
Complete code at the end of this chapter
package main
import ( "log" "net/http" "os"
oicana "go.oicana.com/oicana")
func main() { templateBytes, err := os.ReadFile("templates/example-0.1.0.zip") if err != nil { log.Fatalf("Failed to load the template file: %v", err) } // Creating the template runs a warm-up compilation in development mode, // which uses the development value of the input. template, err := oicana.NewTemplate(templateBytes) if err != nil { log.Fatalf("Failed to create the template: %v", err) } defer template.Close()
http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) { pdf, err := template.ExportPdf(oicana.Inputs{ JSON: map[string]any{"info": map[string]any{"name": "Baby Yoda"}}, }) if err != nil { log.Printf("Failed to compile template: %v", err) http.Error(w, "Failed to generate the document", http.StatusInternalServerError) return }
w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`) w.Write(pdf) })
log.Fatal(http.ListenAndServe(":8080", nil))}