Strapi Transform Pipeline
Strapi stores questionnaire data in a structured format optimized for its CMS editing experience — dynamic zones, nested components, and relational fields. The app needs a different, flatter format. The transform pipeline bridges these two worlds.
There are two directions:
- Forward transform: Strapi format to canonical format (used when syncing from CMS to app)
- Reverse transform: Canonical format back to Strapi format (used when pushing changes from app to CMS)
Why Transforms Exist
Section titled “Why Transforms Exist”Strapi’s editing UI works best with:
- Dynamic zones — flexible lists of different component types
- Component nesting — rich structure for complex fields
- Relational fields — links between content types by ID
But the app and qnr-framework work best with:
- Flat arrays — simple lists with a
typefield - Plain objects — key-value pairs instead of arrays of
{key, value} - Resolved references — names and values instead of IDs
The transform pipeline handles this mismatch so that each side gets the format it works best with.
Forward Transform (Strapi to App)
Section titled “Forward Transform (Strapi to App)”The forward transform happens in Strapi’s questionnaire.js service. When the API returns a questionnaire, this service reshapes the data before sending it.
Meta Constants
Section titled “Meta Constants”Strapi format — an array of key-value objects:
[ { "key": "minAge", "value": "18" }, { "key": "maxCoverage", "value": "50000" }]Canonical format — a flat object:
{ "minAge": "18", "maxCoverage": "50000"}Why: A flat object is simpler to look up (constants.minAge) than searching through an array.
Question List and Nested Question IDs
Section titled “Question List and Nested Question IDs”Strapi format — a questionList array plus separate nestedQuestionIds entries:
{ "questionList": ["intro", "dateOfBirth", "address"], "nestedQuestionIds": [ { "parentId": "address", "children": ["street", "city", "postalCode"] } ]}Canonical format — merged into a single list with nested arrays:
["intro", "dateOfBirth", ["address", "street", "city", "postalCode"]]Why: The framework needs a single ordered list to determine question sequence, with nesting expressed inline.
Progress Bar
Section titled “Progress Bar”Strapi format — array of step definitions:
[ { "initialQuestionId": "intro", "title": "Personal Info" }, { "initialQuestionId": "coverage", "title": "Coverage" }]Canonical format — two mapping objects:
{ "questionnaireIdMapping": { "intro": 0, "coverage": 1 }, "questionGroupsMapping": { 0: "Personal Info", 1: "Coverage" }}Why: The framework needs fast lookups — given a questionId, which progress step are we on?
Questions (Dynamic Zone to Flat Array)
Section titled “Questions (Dynamic Zone to Flat Array)”Strapi format — dynamic zone components with __component field:
[ { "__component": "question.radio", "questionId": "plan", "options": [...] }]Canonical format — flat objects with type field:
[ { "type": "RADIO", "questionId": "plan", "options": [...] }]Why: The __component format is a Strapi internal concept. The app just needs a type string to select the right React component.
Strapi format — rule components with verbose field names:
[ { "questionId": "dateOfBirth", "condition": ["dateOfBirth", "lessThan", "18", "year", "dateDiff"], "outcome": { "goTo": "ageBlocker" } }]Canonical format — simplified:
[ { "id": "dateOfBirth", "if": ["dateOfBirth", "lessThan", "18", "year", "dateDiff"], "then": { "goTo": "ageBlocker" } }]Why: The compact id/if/then/else format is what the rules engine expects and is easier to read at a glance.
Reverse Transform (App to Strapi)
Section titled “Reverse Transform (App to Strapi)”The reverse transform (reverseTransform.js service) undoes all the forward transforms. It is used by:
- Reverse sync — pushing a locally edited
.tsfile back to Strapi - Write-transformed endpoint — API endpoint that accepts canonical format and writes it to Strapi
What It Does
Section titled “What It Does”Each forward transform is inverted:
| Forward | Reverse |
|---|---|
| Constants: array to object | Object back to array of {key, value} |
Questions: __component to type |
type back to __component format |
Rules: id/if/then to questionId/condition/outcome |
Restore verbose field names |
| Progress bar: array to mappings | Mappings back to array |
Resolving Relations
Section titled “Resolving Relations”One of the more complex parts of the reverse transform is relation resolution. In canonical format, relations are often stored as names or slugs:
{ "vertical": "household" }But Strapi needs the internal database ID:
{ "vertical": 42 }The reverse transform looks up these IDs by querying Strapi for the matching entity (quote-page by name, vertical by slug, option-list by identifier, etc.).
The Full Round Trip
Section titled “The Full Round Trip”flowchart LR
A["<b>Strapi DB</b><br/>Components,<br/>dynamic zones,<br/>relation IDs"] -->|"Forward transform<br/>(questionnaire.js)"| B["<b>Canonical Format</b><br/>Flat types,<br/>plain objects,<br/>resolved names"]
B -->|"Sync script<br/>+ t() wrapping"| C["<b>Generated .ts</b><br/>TypeScript file<br/>in app repo"]
C -->|"Reverse sync<br/>+ parse"| B
B -->|"Reverse transform<br/>(reverseTransform.js)"| A
The pipeline is designed to be lossless — a questionnaire that goes through forward transform and then reverse transform should produce the same result in Strapi. This is important for the reverse sync workflow, where Product teams may edit the generated TypeScript file locally and push changes back to the CMS.
When Transforms Can Cause Issues
Section titled “When Transforms Can Cause Issues”| Scenario | What happens | How to fix |
|---|---|---|
| New component type added to Strapi | Forward transform may not know how to map __component |
Update the type mapping in questionnaire.js |
| Relation not found during reverse sync | Reverse transform cannot resolve a name to an ID | Ensure the referenced entity exists in Strapi first |
| Custom fields on a component | May be lost in round-trip if not handled by both transforms | Add handling in both forward and reverse transform services |
See also
Section titled “See also”- Sync Commands Reference – CLI commands that trigger the transform pipeline
- Sync a Questionnaire – forward sync workflow
- Questionnaire Lifecycle – where transforms fit in the full data journey