Pairing Strapi with a separate backend

Praveen Jayakody
NoLoopTech
Published in
6 min readMar 4, 2024

In this article, we discover what Strapi is briefly and then explore an architecture where we have a CMS as well as a separate backend. This article specifically talks about a NestJS backend, but it can be applied to any type of backend.

What is Strapi?

One way to sum up Strapi is that it provides the tools to create different content types and create content using the said content types.

Examples for content type can be an article schema (which contains fields like article, author, content…etc), author schema (which contains fields like name, articles…etc). And content created using these types could be a specific article under a named author.

Content type editor showing the article schema

Strapi architecture

Content types can only be created or edited when Strapi is running in development mode. The interesting part about content types is that these are not stored in the database. Instead they are stored as JSON files in the src folder.

Part of the folder structure showing the content type schema JSON

This means that content types are managed using version control (since they are part of the repo). To sync new content types to your production deployment, you need to pull from the version control.

Setting up Strapi

One way to get started is to create a repo using a template from Strapi . However, if you want to setup a Strapi with TypeScript support:

  1. yarn create strapi-app my-project --ts
  2. yarn to install packages
  3. Copy the provided .env.example and rename it to a new .env
  4. Add database credentials to .env file
# DB
DATABASE_CLIENT=postgres # If using a Postgres DB
DATABASE_HOST=test.postgres.database.azure.com
DATABASE_USERNAME=sampleuser
DATABASE_PASSWORD=pwd
DATABASE_SSL=true

Gotcha: if the database is a Postgres one, you might have to manually install pg package by running yarn add pg to install the driver.

Next, run yarn develop to start Strapi in auto reload mode. Access the admin portal using http://localhost:1337/admin During the first time login, you will be prompted to create an admin user.

Collection types and adding content

A new collection type can be created by clicking on Content-Type Builder and then Create new collection type. When you are creating a new collection type, you are given the option to add API IDs.

First dialog shown when creating a new collection type

Once you press on Continue, you are shown the screen below where you can certain fields of different types.

Add fields to a collection type (article)

And remember, creating a new content type will add new files in our code. Don’t forget to commit these!

After saving this, you can go to the Content Manager to add content from the type we just created (i.e. we can create new articles). Select the Collection Type as Article. You will be shown a list of existing articles (if any) and a Create new entry button.

Screen to create a new article from our collection type

Strapi endpoints

Now that we have our content schema and content in place, how do we retrieve these from our ReactJS or React Native app?

Strapi has a REST API and a GraphQL API. In this article we will be looking at the REST API. How the REST API nomenclature is laid out can be accessed at https://docs.strapi.io/dev-docs/api/rest#endpoints.

By default, API needs to have an authentication of some sort. An API token can be created by going to Settings and then API Tokens. When creating API tokens, you are given the ability to control exactly what permissions and content is accessible by the token.

On each endpoint, the token should be sent on the Authorization header as a Bearer token (ex: Bearer <api-token>)

Authentication and Users

The issue with the API token method of authorisation is that we cannot differentiate between individual users. This situation arises in scenarios where the CMS is only part of the application.

Let’s say that you have a separate NestJS backend where there are certain user tiers. Tier A users can only access Silver Tier articles while Tier B users can access both Silver and Gold Tier articles.

Then we have the React app which consumes the content from our Strapi application but authenticates with the NestJS backend. How do we do permission management on Strapi end of things?

One option (the option that we will be looking at in this article) is to have a user on the Strapi server for every user on our NestJS backend. When a user authenticates with the NestJS backend, we can get a JWT token for the Strapi user as well.

This means that on the React app, there will be two JWTs:

  1. A JWT to send with requests to our NestJS backend
  2. Another JWT to send with requests to our Strapi CMS

The entire login flow and requesting flow can be summarised as the following diagram:

Login flow and requesting flow

Custom authentication endpoint

To create a custom Strapi endpoint, use yarn strapi generate with the API named server-auth. A new folder will be generated and in this folder, change routes/server-auth.ts and change the route path to /auth/server.

Change the newly created router, controller, and service as follows

/* routes/server-auth.ts */

export default {
routes: [
{
method: 'POST',
path: '/auth/server',
handler: 'server-auth.auth',
config: {
policies: [],
middlewares: [],
},
},
],
};
/* controllers/server-auth.ts */

export default {
auth: async (ctx, next) => {
try {
// INFO: parsing
const email: string = ctx.request.body.email;

// INFO: validation
if (!email) {
ctx.throw(400, 'Email is required');
}

await strapi.service('api::server-auth.server-auth').auth(ctx, email);
} catch (err) {
ctx.throw(400, 'Could not authenticate', err);
}
},
};
/* services/server-auth.ts */

export default {
auth: async (ctx, email: string): Promise<void> => {
const userList = await strapi.entityService.findMany('plugin::users-permissions.user', {
filters: {
email,
},
});

if (userList.length === 0) {
ctx.throw(400, 'User not found');
return;
}

const user = userList[0];

const token = await strapi.plugins['users-permissions'].services.jwt.issue({ id: user.id });
ctx.body = {
user: {
id: user.id,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
blocked: user.blocked,
},
access_token: token,
};
},
};

By default, this API route is created as a private route. This means that the API token which we use to call this endpoint (possibly from our NestJS server) needs to have permission to access this endpoint. To grant this permission, create (or modify an existing token) to have the permission to access this endpoint (which would have appeared now):

Grant permission to call our custom API

Make sure you do not expose this API token! Because it has the power to authenticate any user by just providing the email address! However, in our use case, we will only be using this particular endpoint from the server.

P.S. I wrote this article while setting it up for a project so there might be gaps in the instructions. Do reach out to me with questions or feedback and I will make sure to answer them!

Special thanks to Thashwini Wattuhewa for a great intro session into Strapi :)

--

--