We use all of these API Backend frameworks: Strapi, Frappe (ERPNext), NestJS, and WordPress. Strapi is a headless CMS. NestJS is a low-level NodeJS microservice framework. Frappe is a complete framework that powers ERPNext. WordPress is a popular CMS which is extensible.
Now, how do they compare (at least in Lovia’s perspective)? For developing our own custom apps, which one should we use? This comparison attempts to review each framework and explain the reasoning behind our choices.
Criteria | Strapi | Frappe (ERPNext) | NestJS | Meteor | WordPress |
---|---|---|---|---|---|
Type | Headless CMS | Application framework | Microservice framework | CMS | |
Language | JavaScript | Python | TypeScript | PHP | |
Deployment Cost & Scalability | Manual using Docker / Kubernetes File uploads can use S3 | Manual using Docker / Kubernetes and NFS/Ceph | Serverless-ready Recommended: aws-serverless-fastify Default: aws-serverless-express Note: Does not support GraphQL subscriptions | On VM: WordOps Docker on Kubernetes with NFS/Ceph shared folder | |
Database | MongoDB, MariaDB, PostgreSQL, MySQL | Recommended: MariaDB Alternative: MySQL Frappe only, not ERPNext: PostgreSQL | MongoDB, MariaDB, PostgreSQL, MySQL, Elasticsearch, Redis, and any other database | MariaDB / MySQL | |
Multiple databases | Yes | No | Yes | Custom code | |
Multiple sites per instance | No | Yes | No | Only if using Multisite Network | |
REST API Support for Custom Types | Automatic | Automatic | Manual via Controllers | Partial: Adding REST API Support to Existing Content Types | |
OpenAPI Documentation | Automatic | None | Manual via @nestjs/swagger | None Contributed plugin: WP API SwaggerUI | |
GraphQL Support for Custom Types | Automatic | None | Manual via @nestjs/graphql | None Contributed plugin (for built-in WordPress objects): WPGraphQL | |
File upload storage | Filesystem / Amazon S3 | Filesystem / NFS / Ceph Can mirror to Amazon S3 using contributed app: frappe-attachments-s3 | Custom code | Filesystem | |
PubSub library | None. GraphQL Subscriptions is still PR #6789. | Redis + ? | None. Can be added via Apollo graphql-subscriptions | Meteor | None |
PubSub transport | None | SocketIO (WebSockets, long-polling) | None. Can use Apollo GraphQL Subscriptions over WebSockets. | WebSockets | None |
Improving Portability
We’re trying to use Strapi by default (hopefully to also get future improvements), but also make it usable by NestJS if needed. The main reason is developer and admin productivity (this is similar reason why we use WordPress). If performance is an issue, the recommended approach is to utilize Elasticsearch and/or Redis.
So even when using NestJS, we’re trying to constrain the schema so that it plays well with Strapi. upload_file collection should also follow Strapi conventions. The REST API and GraphQL syntax should also follow Strapi’s conventions.
Strapi users-permissions_user Collection Structure
Note: password
is randomly auto-generated and directly thrown away, never used. We instead use FusionAuth OpenID Connect, through ssoId
field.
These user fields are locked by Strapi:
Field | Type |
---|---|
_id | ObjectId |
confirmed | boolean |
blocked | boolean |
username* | String |
email* | String |
password* | Password String |
provider | Enum: local | … |
role | ObjectId (of users-permissions_role collection) |
createdAt | ISODate |
updatedAt | ISODate |
created_by | ObjectId (of strapi_administrator collection) |
updated_by | ObjectId (of strapi_administrator collection) |
{
"_id":
{
"$oid": "5f2634bb855b5c1676e680a1"
},
"confirmed": false,
"blocked": false,
"gender": "M",
"relationship": "M",
"username": "mananana",
"email": "[email protected]",
"password": "$2a$10$Px8aGpF8G.cZa.qbnPZBH.d8tjxTrvFzbI0qBL2AEqo6YSX6BhoFa",
"addressCity": "Jakarta",
"provider": "local",
"createdAt":
{
"$date": "2020-08-02T03:36:27.973Z"
},
"updatedAt":
{
"$date": "2020-08-02T04:40:10.340Z"
},
"__v": 0,
"created_by":
{
"$oid": "5f26335df01fe80f3fcc8fe1"
},
"role": null,
"updated_by":
{
"$oid": "5f26335df01fe80f3fcc8fe1"
},
"addressCityDisplayName": "Jakarta",
"profilePhoto":
{
"$oid": "5f263527855b5c1676e680a2"
},
"profileVisibility": "P",
"age": 35
}
Strapi upload_file Collection Structure
Reference:
- https://www.thetopsites.net/article/58551844.shtml
- https://stackoverflow.com/a/56689316/122441
- https://github.com/strapi/strapi/blob/master/packages/strapi-plugin-upload/services/Upload.js#L318
- https://github.com/strapi/strapi/blob/master/packages/strapi-provider-upload-aws-s3/lib/index.js#L20
- https://github.com/strapi/strapi/blob/97922f99ae858e1e3f4f792bb8d6a829adb82649/packages/strapi-plugin-upload/services/Upload.js#L128
- https://github.com/strapi/strapi/blob/97922f99ae858e1e3f4f792bb8d6a829adb82649/packages/strapi-provider-upload-local/lib/index.js#L39
Note: Strapi’s thumbnail
for upload_file
is mainly useful for backend. For frontend, we use Cloudimage’s transformation features instead, which provides e.g. face cropping.
{
"_id": {
"$oid": "5f263527855b5c1676e680a2"
},
"name": "hendy-siete-square.jpg",
"alternativeText": "",
"caption": "",
"hash": "hendy_siete_square_3a3520ac33",
"ext": ".jpg",
"mime": "image/jpeg",
"size": 31.58,
"width": 374,
"height": 374,
"url": "/uploads/hendy_siete_square_3a3520ac33.jpg",
"formats": {
"thumbnail": {
"name": "thumbnail_hendy-siete-square.jpg",
"hash": "thumbnail_hendy_siete_square_3a3520ac33",
"ext": ".jpg",
"mime": "image/jpeg",
"width": 156,
"height": 156,
"size": 6.2,
"path": null,
"url": "/uploads/thumbnail_hendy_siete_square_3a3520ac33.jpg"
}
},
"provider": "local",
"related": [{
"_id": {
"$oid": "5f26352e855b5c1676e680a3"
},
"ref": {
"$oid": "5f2634bb855b5c1676e680a1"
},
"kind": "UsersPermissionsUser",
"field": "profilePhoto"
}],
"createdAt": {
"$date": "2020-08-02T03:38:15.466Z"
},
"updatedAt": {
"$date": "2020-08-02T03:38:22.867Z"
},
"__v": 0,
"created_by": {
"$oid": "5f26335df01fe80f3fcc8fe1"
},
"updated_by": {
"$oid": "5f26335df01fe80f3fcc8fe1"
}
}
Using default filesystem, the files will be stored inside public/uploads
subfolder.