DEV Community

Abir Ganguly
Abir Ganguly

Posted on

My experience with a headless CMS (Strapi)

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.

Content Types Builder

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"
}
Enter fullscreen mode Exit fullscreen mode

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.

Query Builder

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.

Strapi Types

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 the User 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 default User model, so that it can provide different auth flows effectively. Simple solution is to just define a One-to-One relation with Seller and Customer.

{
  "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"
    },
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

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 has User as a related field, now the related User won't be populated in response, because find isn't allowed on users. In fact we can't directly fetch the related User from database (bypassing the permission system), due to permissions set earlier - strapi silently excludes the User related info.

  • You cannot just create a Seller instance with a relation to a User 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);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (8)

Collapse
 
lansolo99 profile image
Stéphane CHANGARNIER

Thanks for this feedback, it’s Interesting to see the pros and cons of using Strapi, though this apply for your specific usage and context.

I also bet on Strapi, and used it for a couple of websites. One of those was also an e-commerce.

I had issues sometimes configuring relationship logics within components, some of them had been resolved with the V4 I have no used yet.

I would be interested to know the e-commerce stack you used with the front-end, and how did you host your project (personally, I relied on Snipcart and Heroku at the time).

Collapse
 
abir777 profile image
Abir Ganguly • Edited

At that time our frontend was in Next.js. We used netlify for hosting the frontend (no specific reason, we used it a lot in the past - that's why)

An aws ec2 instance for strapi. This project is to be used in production and we needed multiple features like CDN, storage, e-mail (currently using zoho zepto mail - dirt cheap transactional mail). We were getting all these from a single provider AWS, at a very cheap rate - but definitely it was quite a work to set up. I would rather recommend using something like digital ocean if you can spend few extra bucks.

Later on as we moved off from strapi, we also realised that next.js, along with its app router and RSC is adding some extra complexity, we were SSRing most of the pages - the frontend server felt like merely a proxy in between. Then we decided to move to good old Django + HTMX. I will post a separate post about all these, the good, the bad - all of that.

Collapse
 
lansolo99 profile image
Stéphane CHANGARNIER

Thanks for the details! I wouldn't have think to AWS for hosting Strapi, neither Netlify, as Vercel seems more the way to go with Next. I will have a look at the other tools you mentioned. So I guess the e-commerce part is handmade plugged with a Stripe or something...

Thread Thread
 
abir777 profile image
Abir Ganguly

We use different provider for Payments and shipping, as Stripe doesn't support some Indian payment methods.

I am just curious about Next.js specifically its backend part. I heard about Vercel edge or something. It probably has some major limitations (maybe like time limit or storage?) Kinda obvious because it is mostly meant for frontend.

We had to use a proper backend hosting strategy - there are webhooks coming from external payment and shipping APIs, background cron jobs, backups, also long running tasks like websockets - can Vercel edge handle it? I haven't used it.

Thread Thread
 
lansolo99 profile image
Stéphane CHANGARNIER

I'm not sure about Vercel capacities, as it's moving very quickly. Storage is pretty new, I know edge functions (internal next api routes actually) are improving as well but I didn't have chance to experience all those stuff yet. I know for sure it's not mean to host a strapi instance as of now 🙂.

Collapse
 
darkmortal profile image
Saptarshi Dey

I'll definitely try out this thing

Collapse
 
xanwtf profile image
Xan

Interesting post! I'd definitely just be updating the included User model, just like with Laravel or Rails.

Collapse
 
rickrickiin profile image
Rickrickiin

Intresting post