Uncharted Territory JSS and Headless
Our development team was not only new to JSS as a technology, but also to the concept of developing within a headless mindset. When you look at how JSS exposes data structures out-of-the-box, it does that by exposing Sitecore data templates to the outside world. Although you can influence this by leveraging integrated GraphQL, the main source of truth still lays within Sitecore. For the frontend developers to build their components, they need to know about those Sitecore structures. Now, within our development teams, we have a rather traditional split between backend and frontend developers. The frontend developers have no local Sitecore instance running, are almost exclusively working on MacBooks and don't necessarily know much about Sitecore specificas. With an unedited setup of the JSS workflow, we would have a massive dependency of the frontend development to the state of the Sitecore environment and its data structures.
Interface between Frontend and Backend
Based on the situation outlined above, we asked ourselves a few questions before even starting the development of the platform:
How can we decouple the frontend and backend development as much as possible?
How do we share the information about what data is available to the frontend components?
Do we want to expose the Sitecore internal data structures or do we want an additional separation?
View Models connect the two worlds
If you break out of our cozy Sitecore environment and take a look at a classical ASP.NET MVC project with, let's say the use of the Entity Framework, then you would most likely prefer to not expose the data models from the database context to the outside world. You would consider introducing view models to avoid a direct dependency between the data structure and the data available to the frontend components. In our context, the Sitecore data templates are those data structures and the layer that represents the view models does not exist. This paves the way to introduce that additional layer of view models, which connects the frontend and the backend. It glues it together and becomes the shared truth about data structures.
But as long as we have no way to formalize the view model, we have not gained much. Of course we can establish a verbal contract between the backend and frontend development, but such a verbal understanding is tricky to maintain, vague and prone to misunderstandings. To avoid this, we introduced a contract that is technical enough to be precise, but easy enough to understand for multiple roles within the project to contribute. Then, we came up with a solution that lets us generate the view models from the contract as C# classes for the backend and React proptypes for the frontend.
Creating a Common Language
The contract consists of the final view models, but also of its building blocks. The latter could be either a Sitecore field type or any partial model that can be reused throughout the contract. As a first step, let's have a closer look at the field types.
A crucial requirement for us was to not lose the possibility to leverage the Experience Editor, Sitecore's WYSIWYG-Editor. To achieve that, we needed to provide a representation for the field types. Most of those are pretty straight forward, like a simple text field.
As you can see from this snippet, the contract is a markdown file, which uses titles with optional selectors for meta information about a class and a yaml block for the technical specification of the model. The contract markdown file is a CommonMark document that has embedded code-blocks for machine-readable sections. When we take a glimpse at the contract for a view model, we can see how those building blocks can be reused.
AutoRest turns Contract into Code
For the code generation we used AutoRest. While AutoRest is primarily a tool to generate client libraries for accessing RESTful web services, we use its literate file-format support feature to turn our contract into code.
AutoRest accepts a configuration file with instructions. We ended up with the following instructions:
This file defines which file should be used as the contract with the setting input-file. The main output of this process when kicked off is the models as C# classes. But it also produces an intermediate artifact swagger-document.json which is an OpenAPI specification of the contract. This file can then be used by openapi-proptypes-generator that has been open sourced by Unic to generate the React proptypes:
Furthermore, the file defines a few directives to influence the code generation. Because all valid OpenAPI specs need to define operations, we added a dummy entry in the contract. The first directive removes all operations from the generation process, as we don't need it in the generated code. The second directive introduces a convention, where every model from the contract gets the ViewModel postfix, e.g. the flightProcess contract model would end up as a FlightProcessViewModel class in C#. The third directives removes all classes that are not the models themselves, as we are only interested in the models. We identify those classes by the fact that they include the Models namespace. Finally, we simplify the namespace with the last directive.
Using Model Builders as Transportation Service
Now we have all the data structures in Sitecore – nicely adhering to Sitecore Helix of course – and we have the contract defining how the frontend components can expect that data. This leaves us with the question on how to get the data from one point to the other? To address this question we chose to build our software architecture around the concept of Model Builders. In a nutshell, Model Builders take data from Sitecore items and map them onto the view models. We use Synthesis to be strongly typed and we introduced different types of Model Builders to be in line with the Helix architecture.
In the Feature Layer we have our first Model Builder, that takes the Synthesis representation of the interface template as an input and maps it onto a class we call a Carrier Model. The main purpose of this model is to carry through the data to the Project Layer, where it is composed into the final View Model.
This first Model Builder leverages the Model Factory to build its nested models and ensure a high level of reusability. You can read more about this in the three part series here: part 1, part 2, part 3.
In the Project Layer, we have the final Model Builder which is responsible for the composition of the View Model. In the straight forward example of the flight process, it would only create the Carrier Model and assign it to the View Model. If the View Model would need data from multiple features, this would be handled on this level.
Enhancing the Development Process with Contract Driven Development
Looking back at this approach with a bit of distance, we can still say it was a successful and useful approach to build a JSS site. It completely changed our way how not only frontend and backend developers worked together, but also how they worked together with our requirement engineers and business analysts. With the contract, there was a simple agreement between the parties and with the use of code generation we could avoid any miscommunication. For our strongly types backend code, changes in the contract would immediately be spotted, as they would lead to compilation failure. This turned out to be a pretty nice side effect as well.
In almost all cases it was possible for frontend and backend development to work independently and without being blocked by each other. The only requirement is and was, that the contract is more or less done and stable. Changes to it had to be coordinated between the developers and the requirement engineers, as they might have had an impact on other components or specifications down the line.
Contact for your Digital Solution with Unic
Are you keen too discuss your digital tasks with us? We would be happy to exchange ideas with you: Jörg Nölke and Gerrit Taaks (from left to right).