I was approached by my friend to develop a E-commerce site for his book publishing business. I was mainly responsible for the back-end part. Requirements were basic:
- A good looking frontend.
- Multivender (multiple book Publishers / Sellers).
- Admin panel.
- Usual E-commerce stuff: Orders, payments, refunds, shipping, invoice generation etc.
Initially we started off with next.js in the frontend, with postgresql , Typeorm and express.js in backend. We knew from day 1 that it might take a huge time to develop, especially the admin panel, so we were looking for alternatives. Strapi - an Open Source headless CMS was gaining some popularity back then. We gave it a try.
tl;dr: Strapi is an amazing product, but we had some special requirements, which a general CMS couldn’t handle, it has some limitations. Thus, we had to change our techstack, but we learnt a lot in the process.
What is Strapi and what is a headless CMS anyway?
Lets compare it with wordpress, to have easier understanding:
A headless CMS is a content management system (this part is somewhat similar to wordpress) that stores and manages content but doesn’t dictate how it’s presented on a website or app. It lets developers pull content through an API to display it however they want (that’s where it differs from traditional CMS like Wordpress), giving flexibility in design and platform.
“head” = the front-end presentation layer.
“body” = the back-end content management system.
Now, “head-less” = back-end content management system without the presentation layer. We have to develop the presentation layer ourselves.
Strapi is such a headless CMS. There are others in the market like: Contentful , Sanity , Picocms etc. We went with the Open Source and most popular one.
What we appreciate
- It has many functionalities like an admin panel, multiple authentication and authorization methods and a lot more. I have listed a few in this article.
- It also has good plugins and providers like AWS S3, image optimizers, image uploaders, SEO, editors and it is increasing day-by-day.
- Best of all, it is open source, self-hosted and very customizable . We can customize the admin frontend (GUI) and the backend API as well.
Content Types
We can define multiple content types : Single types, Collection types and Components.
- Single type can be like Footer, Header etc.
- Collection type are Posts, Authors, Orders etc.
- Then Components are mainly used for dynamic parts of a website like a banner with CTA and image, FAQ, Carousel. We can basically define a whole part of a webpage using components it is very powerful.
What I learnt is that many sites actually use such CMS in the backend to handle dynamic parts of their site (discounts, banners, CTA), which is mostly set by editors, sales and marketing team.
Image Optimization
Images can be stored in multiple formats like large, medium, thumbnail etc, for faster loading time and all of this is handled by strapi itself using the file upload plugin.
On going to: http://localhost:1337/api/upload/files/1
, we get:
{
"id": 1,
"name": "query_builder.png",
"alternativeText": "Query Builder Image",
"caption": "Query Builder",
"width": 600,
"height": 576,
"formats": {
"thumbnail": {
"name": "thumbnail_query_builder.png",
"hash": "thumbnail_query_builder_7d88426f22",
"ext": ".png",
"mime": "image/png",
"path": null,
"width": 163,
"height": 156,
"size": 12.1,
"url": "/uploads/thumbnail_query_builder_7d88426f22.png"
},
"small": {
"name": "small_query_builder.png",
"hash": "small_query_builder_7d88426f22",
"ext": ".png",
"mime": "image/png",
"path": null,
"width": 500,
"height": 480,
"size": 67.21,
"url": "/uploads/small_query_builder_7d88426f22.png"
}
},
"hash": "query_builder_7d88426f22",
"ext": ".png",
"mime": "image/png",
"size": 13.03,
"url": "/uploads/query_builder_7d88426f22.png",
"previewUrl": null,
"provider": "local",
"provider_metadata": null,
"createdAt": "2024-01-28T07:56:48.469Z",
"updatedAt": "2024-01-28T07:56:48.469Z"
}
API query and filtering
One of the best thing about strapi is their filtering and query functionality, learnt a lot from there. They use the qs library to handle complex filtering use cases. See here . Also, they have a very impressive query builder . Probably I will use them in a future complex project.
There are more such features . We listed the ones which we have used.
Pain points
Most of the bugs we faced is already present in their Github issues.
One of the most surprising bug is that client can update whatever and however they like, even API clients can update id (primary key) of the model. Related issue
Type System
Strapi uses Koa
under the hood. To customize controllers
, you have to work with a ctx
(context) object. This wasn’t clear until you search through the docs properly. They have just mentioned some examples, I hope they just mention that the ctx is from koa in the Customizing Controllers page
, then we could have customized as per our liking. Although this might be a nitpicking (or a skill issue from my side đ)
Also, VS Code doesn’t provide intellisense even if we use Typescript. You need to install @types/koa to get suggestions.
Primary Keys
Anything which isn’t alphanumeric can’t be primary key (like slug or UUID can’t be primary keys). Related issue . This isn’t a big issue, we can circumvent this by creating custom controllers.
JWT Refresh Tokens
JWT tokens are implemented, but there is no refresh token feature as of now (another example of a feature from a forum / blog ). JWT access tokens are expired after 30 days.
Dead End
We are trying to build a multi-vendor site. The default User
model wasn’t enough. What a typical database schema design would do is just to inherit the User
model. In SQL database terms it is just to declare a One-to-one field with the User
model, thus maintaining a relation with the original User
model.
Why not just add required fields to the original
User
model itself? No this isn’t a scalable schema design. Imagine updating theUser
table constantly if new fields need to be added for seller or customer. This isn’t ACID compliant.
Can’t you just use an altogether different user-defined
User
model? No we can’t, strapi is closely tied to its defaultUser
model, so that it can provide different auth flows effectively. Simple solution is to just define a One-to-One relation withSeller
andCustomer
.
{
"kind": "collectionType",
"collectionName": "sellers",
...
"attributes": {
"description": {
"type": "text",
"required": false
},
"user": {
"type": "relation",
"relation": "oneToOne",
"target": "plugin::users-permissions.user"
},
"books": {
"type": "relation",
"relation": "oneToMany",
"target": "api::book.book",
"mappedBy": "seller"
},
...
}
}
Strapi has some permission settings through its permission plugin .
- We allowed find and findOne permission for sellers.
- Only findOne for customers, as usual.
- We only applied findOne for User model (not find, because we don’t want clients to enumerate all of our users - obviously).
Here comes the problems:
Seller
hasUser
as a related field, now the relatedUser
won’t be populated in response, because find isn’t allowed on users. In fact we can’t directly fetch the relatedUser
from database (bypassing the permission system), due to permissions set earlier - strapi silently excludes theUser
related info.You cannot just create a
Seller
instance with a relation to aUser
instance, again for permissions. You must fire another api request just to “link” the two models.Client (browser) could send related fields like
User
, and it would be updated silently, to fix that add custom code (this isn’t a bug - this is definitely expected, just our use case was different).
async update(ctx) {
// ignore the userId passed from client
// it is already set while creating
// (client should not be able to set the userId)
delete ctx.request.body.data.user;
return await super.update(ctx);
}
Conclusion
At last, I will thank all the contributors to the strapi project. It is a wonderful project (Star Here ) and I learnt a lot from their work.
They are doing a wonderful work. Open source is mostly a thankless job - where you have to manage a huge community, constant pull requests, feature requests, issues, rewrites and a lot more.
Here I just shared my experience. We observed that this CMS might not be well suited for our project, but strapi has a lot of scope and use cases in various other projects.
We moved on to a different stack altogether (Django + HTMX), why we did that? What about its scalability? How we did that? Stay tuned đ
Thanks a lot for reading. Stay safe, Have a nice day.