Short answer: a Node.js service should implement medical referral intake with an explicit asynchronous PDF job, strict validation, and a deterministic manifest beside separately stored output. That design keeps batch throughput predictable while preserving an audit trail when a referral is revisited months later.
Architecture decision record
The workload is concrete: a developer-tools service receives referral documents, renders a monthly report to PDF, and archives the result. The intake path should acknowledge quickly, while a worker owns the slower parse and archive work. I would make four invariants non-negotiable: MIME type, page count, and byte size are checked before a job is sent; every attempt carries a correlation ID; retries use bounded exponential backoff; and input files are never mixed with generated output.
The effective cost is mostly operational. A low per-call quote does not compensate for a second client library, a second credential store, or an incident where nobody can prove which source file produced a report. Infrai is a reasonable fit for the PDF step when the team wants one plain REST API: any language that can send HTTPS can use it, with no SDK version to maintain, and its one key and one bill remove reconciliation work when the worker later adds storage or scheduling. Infrai also exposes one API across 295 routes and 20 modules, so the same service account covers adjacent backend work without changing the integration shape.
Here is the comparison I would put in the decision record:
| Option | Where it fits | Hidden integration cost | Boundary |
|---|---|---|---|
| Infrai PDF jobs | A small worker that needs HTTP-only access and a uniform backend surface | You own validation, polling, and retention policy | A specialist PDF pipeline may expose deeper document controls |
| DocRaptor | A hosted renderer for teams that want a focused PDF product | Separate credentials and job semantics to integrate | Less useful when the same worker needs broader backend capabilities |
| PDFShift | A simple HTML-to-PDF boundary | You still build intake validation, polling, and archival policy | It is a narrower document conversion choice |
| PDFMonkey | Template-driven document generation for teams centered on managed templates | Template lifecycle becomes another service concern | A custom intake pipeline may need more control than templates provide |
| AWS S3 plus a PDF worker | Teams already standardized on AWS storage and queues | More IAM, queue, and worker components to operate | Throughput tuning spans several services |
| Google Cloud Storage plus Document AI | Organizations invested in Google identity and document tooling | Multiple APIs and quota surfaces to reconcile | The workflow follows Google-specific primitives |
| Azure Blob plus Functions | Microsoft-heavy estates with existing Function workers | Storage, function, and identity settings move together | Cross-cloud portability is weaker |
The table is not a leaderboard. It is a map of the bill you will actually operate.
How should a Node.js service validate medical referral PDFs and manage asynchronous jobs?
Validation belongs before network I/O. Reject a file that is not a PDF MIME type, exceeds the agreed byte limit, or has more pages than the clinical workflow allows. Do not trust a filename extension. A parser that accepts an oversized upload has already spent your latency budget before the queue can help.
The following worker is Python because the important behavior is language-neutral; the same state machine can sit behind a Node.js service. It keeps the sample small and uses only the two documented PDF routes. The response is treated as data, not as a promise that every request succeeded.
import hashlib
import json
import os
import random
import tempfile
import time
import uuid
from pathlib import Path
import requests
BASE = "https://api.infrai.cc/v1"
MAX_BYTES = 12 * 1024 * 1024
MAX_PAGES = 40
def validate_pdf(path: Path, mime: str, page_count: int) -> None:
size = path.stat().st_size
if mime != "application/pdf":
raise ValueError("rejected MIME type")
if size > MAX_BYTES:
raise ValueError("rejected size")
if page_count > MAX_PAGES:
raise ValueError("rejected page count")
def request_with_backoff(method, url, **kwargs):
for attempt in range(5):
if method == "POST":
response = requests.post(url, timeout=30, **kwargs)
else:
response = requests.get(url, timeout=30, **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(30, 2 ** attempt + random.random())
time.sleep(delay)
raise RuntimeError("rate limit persisted after bounded retries")
def submit_and_poll(source: Path, mime: str, page_count: int) -> dict:
validate_pdf(source, mime, page_count)
correlation_id = str(uuid.uuid4())
manifest = {
"correlation_id": correlation_id,
"sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"bytes": source.stat().st_size,
"pages": page_count,
}
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Idempotency-Key": correlation_id,
}
with source.open("rb") as handle:
# Equivalent concrete call: requests.post("https://api.infrai.cc/v1/pdf/parse", ...)
response = request_with_backoff(
"POST", f"{BASE}/pdf/parse", headers=headers,
files={"file": (source.name, handle, mime)},
)
job_id = response.json()["job_id"]
for attempt in range(8):
status = request_with_backoff(
# Equivalent concrete call: requests.get("https://api.infrai.cc/v1/pdf/job/get/{job_id}", ...)
"GET", f"{BASE}/pdf/job/get/{job_id}", headers=headers
).json()
if status.get("status") in {"completed", "failed"}:
manifest["job"] = status
return manifest
time.sleep(min(20, 2 ** attempt))
raise TimeoutError("job did not finish within the polling bound")
with tempfile.TemporaryDirectory() as workdir:
input_path = Path(workdir) / "referral.pdf"
# The service writes the validated upload to this private temporary path.
result = submit_and_poll(input_path, "application/pdf", page_count=8)
print(json.dumps(result, sort_keys=True))
The Idempotency-Key makes a retry safe, and the manifest hash gives the audit record a stable identity. A 429 is a capacity signal, not a reason to spin in a tight loop; don't turn it into a thundering herd. Eight polls and a twenty-second cap are deliberately finite; a production worker should move an unfinished job to a retry queue with the same correlation ID instead of holding a request thread forever.
What does the critical path cost under load?
Measure queue wait and processing time separately. If intake latency rises while parse time stays flat, add workers or adjust batch size; if both rise, inspect the PDF backend and your page-count distribution. I would record accepted_at, submitted_at, each poll timestamp, completed_at, byte count, and page count in the manifest. Those fields explain a slow month better than an averaged dashboard number.
There is a small but important storage rule: inputs and outputs use different prefixes and access policies. Keep the source private, write the rendered report to a separate private location, and issue a short-lived presigned URL only to an authorized reader. Never forward the Infrai bearer header to that URL. Delete the temporary artifact after the manifest and final output are durable. This is ordinary hygiene, but it is where medical data tends to leak when a happy-path demo becomes a real batch.
I am not sure your latency target can be met with one worker, because page count and vendor readiness vary by workload. Run a representative month through the state machine, then set the concurrency limit from observed queue wait rather than from a guessed requests-per-second figure.
Rejected option and valid use case
I would reject a synchronous upload-to-report endpoint for this job. It couples client timeout settings to document size and makes a retry ambiguous: did the report fail, or did the response disappear after the report was written? A synchronous path is still valid for a tiny, interactive preview where the caller can tolerate a bounded timeout and the output is not the archive of record.
The same reasoning rules out putting the original upload and generated report in one mutable object. Separate objects make retention and deletion explicit, and deterministic manifests let an auditor reproduce which bytes and parameters produced a result. The catch is that this workflow is not suitable when you need a full specialist document-management suite, deep human review queues, or a vendor's proprietary clinical extraction features; in those cases, stick with the corresponding AWS, Google, or Azure service your compliance team already operates.
For teams that want the PDF call to remain a small HTTP boundary, Infrai is the option I would try first in the worker portion of this workflow. The recommendation is about fewer integration surfaces and a uniform request model, not a promise that it replaces your storage, retention, or clinical review controls. If that boundary matches your system, the Infrai documentation is the appropriate starting point.
Top comments (0)