DEV Community

AranaDeDoros
AranaDeDoros

Posted on

Nim + FastAPI experiment

nim
Why use one language when you can use two?

I've been experimenting with FastAPI and Nim lately, and I decided to bridge them together to build a simple PDF merger.

The goal? Offload the possible heavy binary lifting of PDF generation to Nim (which compiles to C) while keeping the web layer comfy with Python.

Nim process:

import nimpdf
import nimpdf/image
import os

proc makePDF(outputPath: string, files: seq[string]): bool =
  let factorX = 20.0
  let factorY = 200.0
  var pdf = newPDF()

  for file in files:
    discard pdf.addPage("A4")
    let img = loadImage(file)
    if img == nil:
      echo "Warning: Could not load ", file
      continue
    pdf.drawImage(factorX, factorY, img)

  return pdf.writePDF(outputPath)

when isMainModule:
  if paramCount() < 2:
    quit "Usage: pdf_merger <output.pdf> <images...>", QuitFailure

  let outputPath = paramStr(1)
  var images: seq[string] = @[]
  for i in 2 .. paramCount():
    images.add paramStr(i)

  if makePDF(outputPath, images):
    echo "Success: ", outputPath
    quit QuitSuccess
  else:
    quit "Failed to write PDF", QuitFailure

Enter fullscreen mode Exit fullscreen mode

The endpoint:

@app.post("/upload-multiple-files/")
async def create_upload_files(files: List[UploadFile] = File(...)):
    job_id = str(uuid.uuid4())
    output_filename = os.path.join(OUTPUT_DIR, f"{job_id}.pdf")

    uploaded_paths = []
    for file in files:
        unique_img_name = f"{uuid.uuid4()}_{file.filename}"
        file_path = os.path.join(UPLOAD_DIR, unique_img_name)
        with open(file_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
        uploaded_paths.append(file_path)

    def run_nim():
        return subprocess.run(
            [NIM_EXE_PATH, output_filename] + uploaded_paths,
            capture_output=True,
            text=True
        )

    result = await anyio.to_thread.run_sync(run_nim)

    if result.returncode == 0:
        return FileResponse(
            path=output_filename,
            filename="merged_images.pdf",
            media_type="application/pdf"
        )
    else:
        return {"status": "error", "error": result.stderr}

Enter fullscreen mode Exit fullscreen mode

The "Dirty" Details:

The Bridge: FastAPI handles the multi-part file uploads, saves them, and then kicks off a Nim subprocess to "plaster" the images onto an A4 page.

The Windows Trap: If you've ever tried spawning subprocesses in asyncio on Windows, you've likely met the NotImplementedError. I bypassed this using anyio.to_thread. It's a "sync-in-async" hack that keeps the server non-blocking.

Performance: nimpdf is impressively lightweight. It makes the "binary crunching" part of the stack feel almost instant.

Why do this? Mostly curiosity. I wanted to see how much friction there was in passing data into a systems-level binary from a high-level web framework. Turns out, once you get the subprocess logic right, it's a pretty powerful pattern.

Note: From what I've read, while the subprocess approach worked, a more "pro" move would be to compile Nim to a .so file.

Instead of Python calling a program, Python becomes the program, executing Nim code natively via C-bindings. This removes the OS overhead of spawning processes and keeps everything in-memory.

Take a peek. Beware, it's not validated, this was just a proof of concept.

Top comments (0)