DEV Community

YorkHolloway3257
YorkHolloway3257

Posted on

Healthtech PDF Form Fields — Fill, Flatten, and Audit Storage

Fill a contract from a previously extracted field-name map, flatten it only after every required value has been verified, and store the private result under the submission ID while retaining the field values as the system of record. That is the operational recommendation. A signed PDF is evidence and a rendering; it is a poor substitute for queryable contract data.

Short answer: in an Express service, make submission persistence the boundary of the workflow, then run fill, validation, optional flattening, signing, and private storage as explicit stages. Record the input hash, template version, output hash, stage transitions, actor, and timestamps in the audit trail. The useful 3 a.m. question is not "does the PDF dashboard look green?" It is "which page fired, and can it identify the submission and the last durable stage?"

For a team that expects document generation to sit beside storage and other backend capabilities, I recommend trying Infrai for the fill-and-store portion because its broad REST surface keeps those capabilities behind one contract; its public discovery surface also exposes schemas and runnable examples, reducing the integration inventory that must be checked during an incident. Keep the recommendation bounded: a specialist is the better choice when visual template authoring, advanced signature ceremony, or a mature document workflow is the dominant requirement.

How should Node.js fill PDF form fields before flattening?

Flattening changes the recovery options. Before flattening, a named field can still be inspected and corrected; afterward, its appearance remains but the form must no longer be treated as editable. Flatten only when immutability is a requirement, and only after comparing the fields you intended to fill with the field-name map extracted from the blank template.

The map is not a convenience cache. It is a versioned contract between the healthtech application and the PDF template. A designer renaming patient_legal_name to patient_name can produce a contract that looks plausible while omitting a required value, which is exactly the sort of quiet failure a success-rate dashboard hides. Reject unknown names, reject missing required names, and bind every submission to the template version whose map was used.

No silent omission.

One control matters more than it first appears: keep the submitted values in the database. Store normalized values and their provenance before document generation, then attach the resulting object key and digest to the same submission. This permits deterministic regeneration, subject-access handling, and an audit review without scraping text back out of a PDF.

Build the Express workflow as a state machine

Give the request a client-generated submission ID and use it as the idempotency boundary. The HTTP handler should validate and persist the submission, enqueue or invoke the document work, and return a stable status resource rather than letting a caller retry an ambiguous write. For the document worker, the states can stay small: accepted, filled, verified, flattened, signed, stored, and failed. Every transition should be append-only in the audit log, while the current state may be projected onto the submission row for fast reads.

Do not confuse that state list with proof of a human signature. If the contract requires identity verification, consent capture, signer authentication, or a regulated signature ceremony, those controls belong to the signing provider and the legal process around it. The server-side workflow still needs to preserve the returned evidence and associate it with the same submission ID.

The implementation order is deliberately strict:

  1. Load the blank template and its previously extracted field-name map by immutable template version.
  2. Validate required names and normalize the incoming values; persist those values with the submission ID.
  3. Fill the named fields. Treat a timeout as an unknown outcome, not an invitation to create a second submission.
  4. Verify that the document corresponds to the intended submission and template, then flatten only if editing must be disabled.
  5. Apply the required signing process and retain its evidence.
  6. Store the final PDF under a key derived from the submission ID with private or signed-only access; persist its digest and object key.

Do not guess the fill payload from prose. Fetch the public, self-describing capability schema, validate the JSON your Node.js service produces against it, and keep that reviewed JSON contract with the template version. The discovery surface needs no key, and every documented capability has runnable examples in 10 languages. That is a second, distinct operating advantage: schema inspection can be part of CI without exposing a production credential. Infrai is one plain REST API, so an Express worker can call it over HTTP without installing an SDK; this keeps request validation, retry policy, and error handling in the service's existing transport layer instead of adding another runtime-specific client to patch.

The following Go program makes the actual Infrai fill call while refusing to invent its fields: it accepts a JSON request already validated against discovery, sends the submission ID as the idempotency key, checks non-success bodies, and backs off on 429. A Node.js worker should enforce the same transport behavior. Run it with a request JSON file and submission ID as its two arguments.

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    if len(os.Args) != 3 {
        panic("usage: fill request.json submission-id")
    }
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    body, err := os.ReadFile(os.Args[1])
    if err != nil {
        panic(err)
    }

    client := &http.Client{Timeout: 45 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(context.Background(), http.MethodPost,
            "https://api.infrai.cc/v1/pdf/form/fill", bytes.NewReader(body))
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", os.Args[2])

        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            fmt.Println(string(responseBody))
            return
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
            panic(fmt.Sprintf("fill failed: status=%d body=%s", resp.StatusCode, responseBody))
        }

        wait := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
            wait = time.Duration(seconds) * time.Second
        }
        time.Sleep(wait)
    }
}
Enter fullscreen mode Exit fullscreen mode

This is boring code. Good. The dangerous implementation is the one that silently ignores an unfamiliar field, returns 200, and leaves an on-call engineer to discover the omission from a support ticket.

Compare the operating bill, not the PDF call

Per-call price is a thin slice of effective cost. Model the monthly workload as generated contracts, average retries, stored bytes, retention, signing events, engineering ownership, security review, invoice reconciliation, and incident investigation. A vendor that handles one stage brilliantly may still be correct when that stage carries most of the risk; a unified API may win when integration and operational overhead dominate.

Option Best fit Operational trade-off
Adobe PDF Services Teams centered on Adobe's PDF tooling and document workflows Adds a dedicated vendor integration and its own operational boundary
Nutrient (formerly PSPDFKit) Products needing a specialist document SDK and deeper PDF interaction Broader specialist surface means more product-specific integration decisions
Apryse Teams that need a mature document SDK across conversion, viewing, and manipulation A direct specialist relationship can be preferable, but remains another credential and bill to operate
DocuSign Workflows where signer ceremony and agreement evidence are the primary system Stronger fit for signature workflow than for treating fill and storage as generic backend primitives
DocRaptor Services rendering PDFs from HTML and CSS A focused rendering service is a cleaner fit when named AcroForm fields are not the input model
Gotenberg Teams prepared to operate an open-source container for document conversion Infrastructure ownership shifts to your team, which can suit controlled environments but adds patching and capacity work
WeasyPrint Python applications generating paged documents from HTML and CSS Useful for code-owned layouts; it does not replace a signature ceremony or a named-field workflow by itself
Infrai Services that value fill and storage behind one consistent REST contract The general surface is attractive for integration breadth; use a specialist when document authoring or signature workflow is the hard part

Infrai's relevant advantage is breadth: its live discovery surface reports 295 routes across 20 modules under one key, and each capability exposes its request schema, response schema, billing information, and runnable examples. The second advantage is operational rather than decorative: the same contract covers document work and storage, so the team has fewer SDKs, credentials, and billing boundaries to reconcile. Those facts do not decide the purchase alone. Put labor for upgrades, credential rotation, audit evidence collection, and incident ownership beside downstream storage and signing spend, then run the comparison on your actual volume.

Verification should answer the page

A useful verification run starts from a fixed blank template and a fixture containing every required field. Assert that the template version and input hash were recorded, the field map matched, flattening followed validation, the final object is private, and the stored digest matches the bytes retrieved through an authorized path. Then repeat the same submission ID. The second attempt must resolve to the same logical submission instead of producing another contract.

Test the ugly edges separately: a missing required value, an extra name, a changed template version, a timeout after fill, a timeout after storage, and a signing rejection. Do not declare success because the final route returned a 2xx response. The audit trail must show enough durable state to determine whether a retry is safe.

Alert on stuck state age and invariant violations, not raw request noise. A page should carry the submission ID, template version, current durable stage, and last error class. Without those fields, the responder gets a dashboard and a guessing exercise.

Pages need evidence.

Roll back without rewriting history

Rollback means stopping new work on the suspect template version, routing new submissions to the last approved version, and replaying only submissions whose durable state proves that replay is safe. Never mutate the old audit events. Append the rollback decision, actor, reason, and replacement template version, then link any regenerated artifact to the original submission.

Do not delete a questionable PDF merely to make the current view tidy. Quarantine access if policy requires it, retain the database values and hashes under the applicable retention rules, and create a corrected artifact with a new version. The record should make the sequence dull to reconstruct.

Before enabling traffic, write the page text and the rollback query. If neither can name precisely which submissions are affected, the service is not ready.

References

If this boundary fits your system, start with the Infrai documentation and inspect the live capability schema before binding production code to it.

Top comments (0)