Engineering

Appunite & Strapi one-year relationship

For over a year, we've been working on the new Appunite's website. For the main (and only) backend, we've chosen Strapi. How did it work with our frontend application? What were the advantages and what were the drawbacks of using Strapi? We’ve described it all in this article.

The requirements

In the previous version of the website, a bit problematic thing was how the data was provided... in fact, all the content's been kept in .json files. So we needed a better solution - any change in the website's copy required a developer to put it into the JSON file, commit it to the repo, make a PR, deploy...

Another requirement was our "landings" system, which allowed us to create simple landing pages by composing ready components and providing some content. This meant that the new solution should be extensible enough to support some kind of "dynamic" data structures.

And last but not least: our blog. At the time, the posts were just markdown files, prepared, and served with a Phoenix application, which also provided a filtering system and a simple author panel for editing the articles. That meant, our solution of choice should have given us some space to implement the filtering, had a markdown editor, and should have given us the ability to create new user (author) accounts with specific permissions (so that an author could only edit the posts).

The solution

It was clear that we needed a headless CMS (a CMS providing only the data layer and a panel for managing that data). Most of the solutions, though, didn't meet our requirements. For example, Contentful doesn't provide an opportunity to create those "dynamic" data structures (the only option would be a JSON field and putting the content in JSON format...).

The solution that caught our eye was Strapi a self-hosted and very extensible CMS.

What is Strapi?

According to the Strapi docs...

The original purpose of Strapi was to help Bootstrap your API. Now, Strapi is an open-source headless CMS that gives developers the freedom to choose their favourite tools and frameworks and allows editors to manage and distribute their content using their application's admin panel. Based on a plugin system, Strapi is a flexible CMS whose admin panel and API are extensible - and which every part is customisable to match any use case. Strapi also has a built-in user system to manage in detail what the administrators and end-users have access to.

So what does working with Strapi look like? Where does it shine, and where's the space for improvement?

Strapi's pros

1. Extensibility

If we had to select one-word defining Strapi - that would be Extensibility. Strapi is just made to be built on. The whole data model is represented using code and is completely detached from the database - all that denotes that your application will be stored in a repo. In contrast, the database keeps the content, users, etc.

The basic concept in Strapi is API. Every app will consist of many APIs, each containing logic for its routes and optional things like data models (a representation of data in the database). For example, there can be an API for blog posts, another for post authors, an API for the content of a specific page on the website, etc.

Strapi has a repeatable file structure for every of the APIs

You can create the APIs manually, via CLI, or using the Strapi Content-Types Builder.

The routes.json file is essential and defines the connections between the route names (for example /api/posts/[id]) and the controller functions. We can define as many routes as we want and then implement them using the controllers for each content type.

Another thing we get is the controller. A controller is a set of functions, whose role should be handling the requests (for example, getting some data from DB and sending them back to the client). When we generate the API, Strapi automatically gives us the default controller, which handles the CRUD operations (but of course, we can always modify it and modify the default route handlers or add new ones).

Then, an API can have its data model - a representation of its data in the DB. For example, the posts.settings.json file contains the Post model with all its fields, relations, etc. Using the Content-Types Builder lets us select the content type's fields and then generate the whole API for our new content type (that is, the model, controller, etc.).

And then there are the API's services. They're meant to contain functions implementing business logic for each API (to separate the code from controllers, ideally only handling request-specific things).

For each of the API's files, Strapi provides a strapi object containing all the Strapi's APIs (like database's ORM and query engine, built-in utility functions, the services we've created, etc.).

All of this makes up for an excellent batteries-included backend framework for creating content-driven apps with a simple administration panel.

Strapi has all the CMS functionalities and provides an opportunity to extend it on Appunite’s website in a way we want.

2. Plugins ecosystem

With that strong core, the Strapi team has created and maintained some official plugins and has made space for the open-source community to create even more 3rd-party plugins.

The essential one is the authentication ("Users & Permissions") plugin. It manages the user accounts, provides an API with the full authentication flow using multiple strategies and allows to create various roles with different permissions (you can set the permissions for each API route, each data-type operation and for the admin panel, which makes Strapi capable of being a regular backend for an app with user authentication).

Strapi has a RBAC system, allowing to select specific permissions for each role

Other plugins include managing a media library (while keeping the data on a separate server like AWS S3) with automatic image resizing, a plugin for mailing, an internationalization plugin, GraphQL plugin, etc.

Furthermore, you can even write plugins specific to your project and place them locally in the repo.

Plugins can plug into literally every aspect of Strapi - from adding new endpoints and data models to modifying or replacing the admin panel's components. For example, we've used this functionality to add a special button for previewing the posts on our blog without publishing them.

3. Powerful data types

Strapi also outstands simply as a CMS. Remember what I have mentioned about the landings system and the "dynamic" data models? Strapi perfectly supports that!

The first great thing about Strapi's data models are "components". For example, let's say we have a CTA banner across multiple pages on our website, but the banner's title is a bit different on each of them. So we can create a banner component in Strapi, define its data structure (let's say, a title, description, button_name, and button_href), and then reuse the structure between multiple Data-Types. It's important to know that components themselves aren't an entity - it's just a reusable data structure inside a data type.

Another excellent use case for components is just segregating the fields inside a Data-Type. For example, we have our company's About Us page, which consists of 4 sections. The first one is Hero, which's a component existent on basically all the pages. Then we have three different areas with content - all of them are separate components - and thanks to this, our data structure looks like this:

interface AboutUsType {
  meta: Meta;
  hero: HeroType;
  about_us_section: {
    title: string;
    description: string;
    image: Media;
  };
  our_team_section: {
    title: string;
    description: string;
    video_youtube_id: string;
  };
  our_values_section: {
    title: string;
    description: string;
    values: {
      name: string;
      icon: Media;
    }[];
    read_more_button?: string;
    read_more_button_link?: string;
    image: Media;
  };
  case_study_banner: {
    background: Media;
    image: Media;
    category: string;
    title: string;
    button_link: string;
    button_label: string;
  };
  awards_section: {
    title: string;
    awards: { image: Media }[];
  };
  featured_content_section: FeaturedContentSection;
}
Not like this...
interface AboutUsType {
 meta: Meta;
 hero: HeroType;
 about_us_section_title: string;
 about_us_section_description: string;
 about_us_section_image: Media;
 our_team_section_title: string;
 our_team_section_description: string;
 our_team_section_video_youtube_id: string;
 our_values_section_title: string;
 our_values_section_description: string;
 our_values_section_values: {
   name: string;
   icon: Media;
 }[];
 our_values_section_read_more_button?: string;
 our_values_section_read_more_button_link?: string;
 our_values_section_image: Media;
 case_study_banner_background: Media;
 case_study_banner_image: Media;
 case_study_banner_category: string;
 case_study_banner_title: string;
 case_study_banner_button_link: string;
 case_study_banner_button_label: string;
 awards_section_title: string;
 awards_section_awards: { image: Media }[];
 featured_content_section: FeaturedContentSection;
}

which just gives more readability in both the codebase and the Admin Panel.

Components can also be repeatable - we can use them for the repeating data structures on the page - take a look at our_values_section.values from the previous example!

A list of components in the Strapi UI

And the final use case for components, the one that really caught our attention because of the landings system - the Dynamic Zones. Strapi allows us to define a particular zone in the Content-Type, in which we can later pick any component from a pre-defined set. Then, on the frontend side, we just create a mapping between the Strapi components in the Dynamic Zone and the frontend app components, and voilà!

Dynamic component picker in the Strapi UI

A list of dynamic components in the Strapi UI

A thing worth noting is that Strapi's Content-Types also have a feature called relations. And relations are what you'd expect them to be - they create connections between multiple Content-Types - for example, a connection between an Author and their Posts, a relationship between a Post and Category, etc. Relations can also have different types, for instance, One-to-One, One-to-Many, etc.

The difference between relations and components is that relations allow you to set a connection between multiple entities, while components just let you reuse and segment the common data structures.

Strapi's cons

1. Lack of support for database migrations

It's common to have multiple environments you run your apps within - apart from production, you may also want to have a staging and development environment, but Strapi doesn't make it easier. It's been, by far, the most annoying problem we've encountered while using Strapi so far.

A mechanism for database migrations should handle modifications (like renames) in the Content-Types. A nice-to-have would also be an option to copy one Content-Types data between environments which currently is practically impossible, and you either have to fill in the data by hand or just dump and restore the whole database.

2. No "default" deployment option

On the one hand - if you're taking a self-hosted solution, you'll have to host it yourself. On the other hand - a well-supported, preferred deployment option would be an excellent addition. Just like Next.js has its Vercel, Gatsby has Gatsby Cloud, and so on.

The deployment of an app like Strapi requires you to set up a database, backup mechanism, etc.

Strapi currently works on preparing an opinionated cloud platform optimized for painless deployment of Strapi apps, but they still have a long way to go.

There is some info about the planned Strapi Cloud, but it's not available yet

As for now, the easiest solution is using the default Docker image, and deploying it to a PaaS like DigitalOcean, platform.sh or Render

3. While being extensible, also being opinionated

Although we didn't encounter any problems with the app architecture provided by Strapi, we see that as a kind of limitation - Strapi forces a specific file structure in the project and adding new features to your app works by creating a particular file that exports an object of a specific structure.

In our opinion, a better option would be to have an imperative API, in which adding new features (e.g., endpoints) would work by calling something from your code (look at, for example, Keystone.js).

Also, Strapi won't allow you to fully control its configuration - basic changes to things like admin panel's Webpack config are possible, but you don't have a full control over the build and development pipeline. Currently, Strapi can't be as flexible as an app built using backend frameworks like Express.js or Nest.js.

Both of these limitations make adding TypeScript to your Strapi app nearly impossible - and if you managed to create a build pipeline - there aren't any typings available for the built-in Strapi's utilities.

If Strapi wanted to be more of a backend framework - they would have to rethink the architecture to make it more flexible to fit more apps - for example, those already built with other frameworks like Express.js.

4. Bugs

At the beginning of our journey with the new Appunite website, Strapi wasn't the best documented and supported CMS. What's more, we can even say that the first setup annoyed us because of many difficulties. Why? Let's think about the situation when you only need a few commands to set up some tools. Cakewalk, right? But not this time. Following the documentation, we set it up and, at first sight, didn't encounter any issues. Unfortunately, the first attempt to change the Roles & Permissions brought an interesting error which turned out to be a stable version issue. Possible solutions? Inject hotfix or downgrade version. Sounds engaging?

Another bug we encountered later was broken table names of the repeatable components. Spontaneously, we were not able to add any data in one of the Content-Types. What's more, the errors thrown by Strapi didn't tell us anything, which made the problem hard to debug. We finally found out that Strapi had first created a table for this repeatable component and then tried to access it, but with a different name (it was wrongly creating the plural form of the component's name). We resolved this issue by deleting the table Strapi had first created, but it took some time and searching through Strapi's source code.

To sum up - every solution isn't free of bugs, but in the case of Strapi - especially about a year ago - we felt like some of its parts weren't tested enough before publishing the stable version. In a matter of a solution that's aimed up to enterprise apps - bugs like these shouldn't be acceptable. What's good, at least, is the development team that reacts quickly and fixes the bugs in the nearest releases.

5. Community support

Many hours spent on Strapi Slack brought us to conclude that the community didn't want to help. The only answers were from the development team, and most of the articles had been prepared by them, so regular users (developers) weren't engaged in its development.

Recently, they have migrated from Slack to Discord. Maybe this step will make it more developer-friendly? We'll see.

6. Still, large parts of the documentation are poorly maintained

The main parts of Strapi's docs are OK, but the sections about backend customization, creating plugins and some of the guides seem outdated and do not cover all the features.

For example, when creating our plugin for the preview button of the blog posts, we had to search through Strapi's source code because the docs didn't cover the creation of the admin panel's frontend plugins.

Summary

Having the experience gained over the last 1 year, would we select Strapi once again? We think so! Strapi, in fact, is one of its kind. There are lots of things that Strapi does well, and looking at the way Strapi is being developed - the drawbacks are being worked on and about to be improved! If you want to learn more about it - we highly encourage you to take a look at Strapi's website and docs.

Meanwhile, thank you for reading, and see you in the next article! AUUUUUUU