Introduction
As programmers, we often revel in the most technical-complete solutions and prefer writing our services from the ground up. That is both a blessing and a curse: the control, efficiency, and power of writing everything yourself comes at the cost of speed in development. As part of our Cloudflight technical lab, we decided to explore different solutions that help in delivering software faster - rapid development. This is specifically useful when our teams are asked to deliver a functional MVP in a very short time.
If you want to learn more about the challenge please read the introduction article first.
Tech stack
Strapi
Strapi is the biggest selling point of our solution. It is a powerful, open-source Node.js and TypeScript Content Management System (CMS). At its core, it is a shortcut for creating REST or GraphQL APIs, replacing the need to write all the backend code yourself. The CMS part, Content Management System, basically means that Strapi provides an admin dashboard running on a web application where you can view your database data, create new API endpoints, and build your server using an intuitive GUI.
Strapi is the leading open-source headless CMS. It’s 100% Javascript, fully customizable, and developer-first. Strapi
Diving deeper inside the inner workings of Strapi, we found out that it uses Koa as a web framework and Bookshelf.js, which is powered by Knex, as an Object Relational Mapping (ORM). On top of these two Node.js libraries a "Strapi framework" was built, which can be used by the autogenerated code, or programmatically in case you don't want or can't use the dashboard generator for a specific case. This framework provides generic Controllers, Services to handle CRUD requests, and a generic Entity Service to handle database operations, like find, create, update, and delete. This makes it very easy to customize Strapi, and also for the dashboard to generate the code.
Some nice-to-have feature of the generated code is the possibility to add complex filtering and join operations right in the requests. The format of the parameters gets converted by the Entity Service into a Knex ORM query, which then translates into SQL. Another great thing is out-of-the-box pagination.
Customizing Strapi is generally very facile. Most of the time the only thing that the developers need to do is add some extra functions, inside an object, passed as a parameter to the constructors of the Controllers or Services. You can use them for custom validations or business logic, and they have access to the original Controllers generated for that entity, and all the Services and Entity Service, so developers don't have to reinvent the wheel. For more complex use cases custom endpoints and database queries can be written.
import {factories} from '@strapi/strapi';
export default factories.createCoreController('api::reservation.reservation',
({strapi}) => ({
// ...
async find(ctx) {
const {data, meta} = await super.find(ctx);
// validation for admin users
if (ctx.state.user.role.name === 'Admin') {
return {data, meta};
}
// custom validation for regular users
const userId: number = ctx.state.user.id;
if (data.some((el) => el.attributes.users_permissions_user.data.id !==
userId)) {
return ctx.forbidden('Data not created by this user', {});
}
return {data, meta};
},
// ...
}));
This code is essentially creating a controller for a reservation API endpoint, with special handling for admin users and regular users. Admins can access reservation data without restrictions, while regular users can only access data that they have created. If they try to access data created by other users, they receive a "forbidden" response.
None of us had any experience working with Strapi or anything remotely similar before. As such, we used pair programming in the first week of working together. It proved to be an excellent use case of our time, as we all collaborated in understanding how to work with Strapi. Once we all got used to the overall architecture of our app, we only pair programmed on the important stuff. In the end, we believe it is very easy and intuitive to use, as a couple of days of research were enough to use Strapi to its full potential, as a self-sufficient complex backend service.
Plugins
The true strength of Strapi lies in its plugins, which can be added free of charge from Strapi's market (built into the admin dashboard). They add additional functionalities to Strapi, extending its capabilities with no additional code. Some of the most useful plugins we have discovered:
Documentation: generates an OpenAPI document for all of Strapi's endpoints that can either be opened on the browser or be used to generate code down the line. Add this plugin to list all available endpoints and see how to properly make requests to Strapi.
Config Sync: allows programmers to share Strapi settings between environments, like access rights to different operations based on roles, either from the CLI or the GUI. This plugin is a must if multiple people are working on the project, or if you want to deploy Strapi.
Other plugins that were preinstalled in our initial Strapi project:
Content Type Builder: Add new data tables in your database from the Strapi GUI. This is what allows you to build new entities and CRUDs. In Strapi, entities can be Collection Types or Single Types. The difference between them is that Single Types only allow for a single value, acting like singletons, while Collection Types are regular tables that support multiple rows.
Content Manager: Quick, code-less way to see, edit, and delete the data in your database. This may also allow the owner of the application to manage content without redeployment.
Email: Configures the application to send emails. This plugin helps you format your emails and send them further to 3rd party providers.
Media Library: Load images and use them in your API - you can easily store multiple image types and use them on your website.
Roles & Permissions: JWT-based API security and user management system. This adds the User data type in your application, which you can use for authentication purposes. It supports multiple providers and multiple security roles.
Internationalization: Adds the ability to create new locales and set up i18n for your API. Having this plugin allows the owner of the app to localize content without redeployment. The API will return the content for the right locale based on your API request parameters.
Performance
We load-tested Strapi's create reservation endpoint using Apache JMeter to have a reliable, reproducible result. We used a variable number of users, increasing it over time. We haven't experienced any performance issues so far.
Mail Sender
Strapi has a built-in email plugin, but it doesn't automatically send the emails, it just formats them and forwards them to certain providers. There are some providers (node modules) built in, but they are either 3rd party platforms, or they are bare bones (such as nodemailer) so, if you want to handle failures yourself and retry after some time, or you don't want an external service to process your emails, you need to create your own provider. Providers are node modules that extend the functionality of a Strapi plugin.
Since we decided to implement email sending using RabbitMQ and a microservice, our provider pushes the formatted email that it receives from the Strapi email plugin to the specific RabbitMQ exchange. Then, this exchange sends it to an email queue, where it will be read by our email processing microservice.
To reliably send emails we decided to set up RabbitMQ so that we have an email queue, a retry queue, and an error queue. In the email queue, we have the emails that need to be processed by our microservice. If, when trying to send the email an error occurs, the email sender will post the message inside the retry queue if the number of trials to send the specific message is smaller than a set constant. Otherwise, it is going to send it to the error queue. In the retry queue, emails remain for a specific amount of time (they have a time to live), then they get discarded and sent back to the email queue.
This approach guarantees that in most cases our service is going to be able to send the emails, and if not, they will remain in the error queue.
VueJS
All webpages are implemented in VueJS using Composition API. Using our previous experience in Angular and React made VueJS easy to pick up. The official documentation and tutorial were very helpful. To speed up development, we have implemented generic components for all views of our app: a generic form using Vuelidate for validation, a generic table with edit and delete actions, and a generic object details card. Using Pinia, it was also very easy to implement reactive stores.
We decided to use the free version of CoreUI as the main components library. The date picker component of CoreUI is locked behind a paywall, and we struggled at first to find a reliable date picker component for our reservations view. We settled on using the date picker of PrimeVue.
OpenAPI generator
Most web developers have seen the OpenAPI specification before, as it is what Swagger tools use. Naturally, we decided to create this specification for Strapi. Upon adding the Documentation plugin to our project, we gained access to a .json file containing the full list of our endpoints in OpenAPI specification. This .json file is generated every time we start the Strapi development server. As such, we can open this file using Swagger to view and better understand how to use the APIs provided by Strapi. This plugin allows for customization of the resulting specification file, like the ability to include or exclude properties or endpoints.
Ultimately, we used the OpenAPI specification of our API to also automatically generate REST services for the frontend that can communicate with Strapi. The tool we used generates Typescript files which we add to our VueJS project. This was the biggest timesaver in our frontend development. Usually, writing the frontend REST services yourself is a redundant task that should be automatized based on your API of choice's specifications.
Postgres
PostgreSQL is the official database system recommendation in the setup guide, but multiple database engines are supported. We have used Postgres due to personal preference and convenience.
Spring boot - where is it?
The initial tech stack included a self-managed Spring boot application, acting as a man in the middle between the VueJS frontend and the Strapi backend. We removed this layer of complexity in the final solution because Strapi was sufficient enough to secure endpoints and capable enough of adding our app's business logic. Thus, adding Spring would have been redundant and would have just added extra complexity, another possible bottleneck, and would have taken more time to develop. Of course, for larger applications there might be requirements that Strapi might not be suitable for, so a Spring backend might be required as well.
Pros/cons discussions
This is an overview of using Strapi for our backend services:
Pros
Facile implementation of CRUDs for models with simple logic
Easy to use: using the GUI for boilerplate code is very convenient
Free to use for any project, if managed on your own premises
Can be extended with custom implementations (you're not limited by the auto-generated code)
Comes with multiple authentication providers out of the box
Offers a lot of plugins, both official and community-made
Multiple SQL database types are supported
Applications may be both vertically and horizontally scaled
Cons
Less time-efficient than a self-managed service
Lack of documentation - debugging can be slow
Unexpected crashes for the CMS /admin dashboard while editing the schemas of the collections (not a problem for production - modifying the schemas is disabled in production)
Incomplete migrations support - migrations run before schemas are updated since schemas are managed by Strapi and migrations by Knex
Virtually no validations support - need to do custom endpoints
Does not support MongoDB natively
Lessons Learned
There is no tech stack right for every project, but for sure there is a best tool for the job. In our case, considering the functional requirements and the deadline, Strapi did a great job. Using a monolithic architecture and built-in Strapi features was the fastest way for us to deliver the MVP in the given period. We found out about Strapi that it is a very powerful and versatile way of building backend services and using it was a satisfying experience. It is also both database and frontend agnostic. We definitely will consider it in the future for new projects, either as a stand-alone, self-sufficient server or as a microservice, since it is more than suitable for delivering MVPs or for creating a reliable microservice in larger projects. It is capable of reducing development time and it is quite easy to pick up. However, for the best experience, we recommend installing the plugins of your Strapi project immediately upon setup, to prevent possible merge conflicts and other issues during collaboration with your team members.
Please note that there are also small caveats, like the one that we learned about when we stumbled upon difficulties as we tried to introduce complex logic and validations. For instance, we had 2 entities with a one-to-many relationship between them, but found out that Strapi did not support cascade delete in the default API implementation (see feature request). As such, we had to add a database-level trigger to delete the ends of the one-to-many relationship.
Another lesson we learned is that Strapi can scale. If you want to scale your application horizontally, you may use a load balancer like Nginx. For additional optimization, you will want to implement caching between sessions. This can be achieved by using the Rest Cache Strapi plugin with a self-managed Redis server.
Also, we find VueJS to be a technology worth using in the frontend frameworks ecosystem. Previous experience with other component-based frameworks is a plus, but the official documentation and forums are a great way to start. As an event-driven architecture, it is intuitive to learn and use.
Let's roll the credits (in alphabetical order) and wrap up this chapter in our software dev diary: Andrei Cotor, Daniel Todașcă, and Gergely-Péter Mátyás contributed to this exploration. Remember, the code may compile, but the journey is what truly matters. Stay curious, stay coding, and let's keep pushing the boundaries of what's possible, one line of code at a time. Until next time, happy coding, my fellow developers!