In this Part 2 article, we will build upon the Strapi app we built in the Part 1 article How to create a Strapi CMS app to manage content. But, instead of using the Strapi Dashboard Admin UI to manage content, we will take a deep dive into exploring how we can programmatically create content types, manage content, and consume the content via the Strapi CLI and APIs. We will also look at the anatomy of the Strapi app that is created and explore some of the functionalities that it provides out-of-the-box.
In the previous article, we created a strapi-books-api
Strapi app to create and consume an API for books and authors. In this article, we will create a website with some pages to build our bookshelf
app. This app will demonstrate how we can programmatically use Strapi CMS to manage content for web pages using the Strapi CLI and APIs.
What we will learn
We will learn the following things as part of this hands-on lab:
- Create the Strapi app
- Review the Strapi app code
- Define content types
- Expose the content via APIs
- Manage and consume the content via APIs
Pre-requisites
To follow the article and build the app step-by-step, you will need the following:
- Node.js installed
- Only Maintenance and LTS versions are supported (
v14
,v16
, andv18
). - Node v18.x is recommended for Strapi
v4.3.9
and above
- Only Maintenance and LTS versions are supported (
- Node.js package manager
- npm (
v6
only) or yarn (we will use npm in the lab)
- npm (
- Python (needed for using SQLite database)
- The
strapi-bookshelf
app project source code repository
Note: I will be using Node.js v18.13.0
and Python 2.7.15
(since we will be using an SQLite database)
Creating the Strapi app
Let’s create the Strapi app named strapi-bookshelf
which will display the books, and let users manage the books.
I. To create the Strapi project, let’s run the following command in a terminal window:
npx create-strapi-app@4.5.6 strapi-bookshelf --quickstart --no-run
The above command will ask to install the create-strapi-app@4.5.6
package. It will create a folder under the current folder by the project name strapi-bookshelf
.
II. We are passing in the installation type as --quickstart
. This will allow Strapi to use SQLite as the default database. You can choose to use the custom option if you want to use any of the other supported databases. We are passing in the flag --no-run
so that after the project creation, the app is not automatically run.
III. After the above step you should have the strapi-bookshelf
app created under /<your project path>/strapi-bookshelf
IV. Now that the app is created, let’s run the app by running npm run develop
.
V. The above command starts the app, and opens a browser window to http://localhost:1337/admin/auth/register-admin
for creating a new admin user via the UI. We can create an admin user using the UI, but instead, let’s create the admin user using the command line.
VI. Let’s exit the running app by pressing CTRL+C
on your terminal window.
VII. To create a new admin user, run the following command in a terminal window:
npm run strapi admin:create-user -- --firstname=Book --lastname=Admin --email=admin@test.com --password=<password here>
Note: You can omit the --password
option, to set the password interactively via the command line.
VIII. The new admin user is created as seen in the following output:
> teststrapi@0.1.0 strapi
> strapi admin:create-user --firstname=Book --lastname=Admin --email=admin@test.com --password=<password here>
Successfully created new admin
Note: This is a local user and is required to manage content in the Strapi Dashboard UI.
IX. Let’s start the app again by npm run develop
. You will notice that the output suggests going to url http://localhost:1337/admin
to open the admin panel.
X. The browser now takes us to the login page for the Strapi Admin site. Use the email and password we used to create the admin user to login.
XI. Now, you are in Strapi’s Dashboard Admin UI. The dashboard site can be used for all admin purposes.
This marks the completion of the Strapi app creation process. If you want, you can CTRL+C
on your terminal window to exit the running Strapi app. We will start the app again in the next section.
To get in-depth details of using the Strapi Dashboard Admin UI to manage content, please check out my article on How to create a Strapi CMS app to manage content.
Reviewing the Strapi app code
Review the project structure
Let’s examine what we got by creating a Strapi app.
We have a new folder strapi-bookshelf
created with the following folder structure:
cd strapi-bookshelf
tree -L 3 -I node_modules
.
├── README.md
├── config
│ ├── admin.js
│ ├── api.js
│ ├── database.js
│ ├── middlewares.js
│ └── server.js
├── database
│ └── migrations
├── favicon.png
├── package-lock.json
├── package.json
├── public
│ ├── robots.txt
│ └── uploads
└── src
├── admin
│ ├── app.example.js
│ └── webpack.config.example.js
├── api
├── extensions
└── index.js
Review the project files
Let’s examine each of the sub-folders and their content and details.
- The
config
folder holds a bunch of JS files that define configurations for different parts of the Strapi app.- The
admin.js
file reads theenv
data and exports theauth token
and theapiToken salt
. - The
api.js
file defines some configuration parameters for the REST API. - The
database.js
file reads theenv
data and defines the configuration details for the SQLite database. - The
middleware.js
file has the configuration details for the Strapi middleware that are included by default. - The
server.js
file has the configuration details for the Strapi server.
- The
- The
database
folder is empty to start with but has an empty foldermigrations
to hold any database migration files. Migrations are a way to incrementally update the database schema with versioning. It allows the developer to upgrade to a new version or downgrade to an older version of the schema. - The
public
folder is empty to start with but will contain any publicly accessible static files or media assets. It has an empty folderuploads
to hold any uploaded asset files like images or documents. These asset files are shown in the Admin UI under the Media Library and can be used in the app. - The
src
folder is where all the source code for the app is stored. All auto-generated code is stored here under separate folders namely,admin
,api
, andextensions
.- The
admin
folder holds some example files to customize and configure the admin part of the Strapi app. - The
api
folder holds the business logic of the project split into subfolders per API that are defined by the app. We will review the API code structure in the next section after we define the content types. - The
extensions
folder holds files to files to extend any installed plugins. - The
index.js
file at the root of the project exports two functions:register
andbootstrap
.- The
register
function is an asynchronous register function that runs before the application is initialized. This gives the developer an opportunity to extend the code. - The
bootstrap
function is an asynchronous bootstrap function that runs before the application gets started. This gives the developer an opportunity to set up the data model, run jobs, or perform some special logic.
- The
- The
Now that we understand the basic project structure of a Strapi app, let’s dive into working on the demo app, starting with defining the content types.
Defining content types
Strapi allows us to define content types for different kinds of content you might have. The content types are categorized into two main categories: Collection and Single types.
Collection types define the kinds of content that can be visualized as a list i.e. a collection of things. Single types define kinds of content that can hold a single data structure.
Let’s define the content types for our demo app that will have some web pages. We need a home page for general information about the app, and a bookshelf page to display the various bookshelves with books.
We will take a programmatic approach in this article, to create the content types in Strapi using the Strapi CLI.
If you stopped the app in the previous section, let’s start the app by:
npm run develop
Creating a “Homepage” single content type
The home page is a single structure of content so we will define a single
content type named Homepage
. The single content type Homepage
will have these attributes: header
, hero_image
, sub_header
, content
, and footer
.
To create the Homepage
single content type, let’s run:
npm run strapi generate
The output we get is as follows:
> strapi-bookshelf@0.1.0 strapi
> strapi generate
? Strapi Generators content-type - Generate a content type for an API
? Content type display name Homepage
? Content type singular name homepage
? Content type plural name homepages
? Please choose the model type Single Type
? Use draft and publish? Yes
? Do you want to add attributes? Yes
? Name of attribute header
? What type of attribute string
? Do you want to add another attribute? Yes
? Name of attribute sub_header
? What type of attribute string
? Do you want to add another attribute? Yes
? Name of attribute hero_image
? What type of attribute media
? Choose media type Single
? Name of attribute content
? What type of attribute richtext
? Do you want to add another attribute? Yes
? Name of attribute footer
? What type of attribute string
? Do you want to add another attribute? No
? Where do you want to add this model? Add model to new API
? Name of the new API? homepage
? Bootstrap API related files? Yes
✔ ++ /api/homepage/content-types/homepage/schema.json
✔ +- /api/homepage/content-types/homepage/schema.json
✔ ++ /api/homepage/controllers/homepage.js
✔ ++ /api/homepage/services/homepage.js
✔ ++ /api/homepage/routes/homepage.js
Now that the Homepage
single content type is created, let’s go back to the Strapi Dashboard Admin UI site and verify that it was created as we needed.
The Homepage
single content type with all its attributes
Creating a “Bookshelf” collection content type
The bookshelf page will display a list of shelves which in turn will have a list of books. So we will define a collection
content type named Bookshelf
. The collection content type Bookshelf
will have these attributes: name
, and description
.
To create the Bookshelf
collection content type, let’s run:
npm run strapi generate
The output we get is as follows:
> strapi-bookshelf@0.1.0 strapi
> strapi generate
? Strapi Generators content-type - Generate a content type for an API
? Content type display name Bookshelf
? Content type singular name bookshelf
? Content type plural name bookshelves
? Please choose the model type Collection Type
? Use draft and publish? Yes
? Do you want to add attributes? Yes
? Name of attribute name
? What type of attribute string
? Do you want to add another attribute? Yes
? Name of attribute description
? What type of attribute richtext
? Do you want to add another attribute? No
? Where do you want to add this model? Add model to new API
? Name of the new API? bookshelf
? Bootstrap API related files? Yes
✔ ++ /api/bookshelf/content-types/bookshelf/schema.json
✔ +- /api/bookshelf/content-types/bookshelf/schema.json
✔ ++ /api/bookshelf/controllers/bookshelf.js
✔ ++ /api/bookshelf/services/bookshelf.js
✔ ++ /api/bookshelf/routes/bookshelf.js
Now that the Bookshelf
collection content type is created, let’s go back to the Strapi Dashboard Admin UI site and verify that it was created as we needed.
The Bookshelf
collection content type with all its attributes
Review auto-generated code
Now that we created the Homepage
and Bookshelf
content types, let’s review the code that Strapi CLI generated automatically for us.
cd strapi-bookshelf
tree -L 4 src/api
src/api
├── bookshelf
│ ├── content-types
│ │ └── bookshelf
│ │ └── schema.json
│ ├── controllers
│ │ └── bookshelf.js
│ ├── routes
│ │ └── bookshelf.js
│ └── services
│ └── bookshelf.js
└── homepage
├── content-types
│ └── homepage
│ └── schema.json
├── controllers
│ └── homepage.js
├── routes
│ └── homepage.js
└── services
└── homepage.js
At this time, we will focus our attention on the schema.json
file created under the folder src/api/bookshelf/content-types/bookshelf
and src/api/homepage/content-types/homepage
.
Let’s open the src/api/bookshelf/content-types/bookshelf/schema.json
file in any code editor and have a look:
// src/api/bookshelf/content-types/bookshelf/schema.json
{
"kind": "collectionType",
"collectionName": "bookshelves",
"info": {
"singularName": "bookshelf",
"pluralName": "bookshelves",
"displayName": "Bookshelf"
},
"options": {
"draftAndPublish": true,
"comment": ""
},
"attributes": {
"name": {
"type": "string"
},
"description": {
"type": "richtext"
}
}
}
You will notice that the above json structure describes the information that we supplied via the Strapi CLIs generate
command.
Let’s enrich the content type by updating it with some required
& unique
constraints, and a relation
type.
Update content type schema
At this time, Strapi CLI’s generate
command does not allow creating relation
type attributes and setting advanced settings of required
and unique
.
Let’s edit and update the schema.json
files for both Homepage
and Bookshelf
content types to add:
- required
and unique
constraints
- relation
type (Homepage
has a one-to-many
relation to Bookshelf
)
The edited bookshelf/schema.json
file looks like this:
// Edited src/api/bookshelf/content-types/bookshelf/schema.json
{
"kind": "collectionType",
"collectionName": "bookshelves",
"info": {
"singularName": "bookshelf",
"pluralName": "bookshelves",
"displayName": "Bookshelf"
},
"options": {
"draftAndPublish": true,
"comment": ""
},
"attributes": {
"name": {
"type": "string",
"required": true, // <=== added
"unique": true // <=== added
},
"description": {
"type": "richtext"
}
}
}
The edited homepage/schema.json
file looks like this:
// Edited src/api/homepage/content-types/homepage/schema.json
{
"kind": "singleType",
"collectionName": "homepages",
"info": {
"singularName": "homepage",
"pluralName": "homepages",
"displayName": "Homepage"
},
"options": {
"draftAndPublish": true,
"comment": ""
},
"attributes": {
"header": {
"type": "string",
"required": true // <=== added
},
"sub_header": {
"type": "string"
},
"hero_image": {
"type": "media",
"allowedTypes": [
"images",
"files",
"videos",
"audios"
],
"multiple": false
},
"content": {
"type": "richtext",
"required": true // <=== added
},
"footer": {
"type": "string",
"required": true // <=== added
},
"bookshelves": { // <=== added
"type": "relation",
"relation": "oneToMany",
"target": "api::bookshelf.bookshelf"
}
}
}
Now that the Bookshelf
and Homepage
content types are updated, let’s go back to the Strapi Dashboard Admin UI site and verify that it was updated as we needed.
To get in-depth details of using the Strapi Dashboard Admin UI to manage content, please check out my article on How to create a Strapi CMS app to manage content.
That concludes the part of updating the content types. Now, let’s see how we can create data for the content types we defined via the APIs Strapi created for us automatically.
Exposing the content via APIs
Before we can expose the content, we need to set the roles & permissions for the content, so that we can publicly access the content via the APIs.
We want to add content for the homepage and bookshelves. To create and consume the content data via the APIs, we need to make sure that the content is publicly accessible through the API.
We will allow authenticated access to create
, update
, and delete
APIs. We will allow public access to find
and findOne
APIs.
Note: At this time, Strapi CLI does not allow setting roles and permissions programmatically. We will have to use the Strapi Dashboard Admin UI to set roles and permissions for our content types.
Setting roles and permissions for the content
Follow the steps below to give selected permissions to the Authenticated role:
- Click on General -> Settings at the bottom of the main navigation.
- Under the Users & Permissions Plugin, choose Roles.
- Click the Authenticated role.
- Scroll down under Permissions.
- In the Permissions tab, find Bookshelf and click on it.
- Check the create, delete, and update checkboxes.
- In the Permissions tab, find Homepage and click on it.
- Check the delete and update checkboxes.
- Click Save.
Setting roles and permissions for the Authenticated role for the Bookshelf content type
Follow the steps below to give selected permissions to the Public role:
- Click on General -> Settings at the bottom of the main navigation.
- Under the Users & Permissions Plugin, choose Roles.
- Click the Public role.
- Scroll down under Permissions.
- In the Permissions tab, find Bookshelf and click on it.
- Check the find and findOne checkboxes.
- In the Permissions tab, find Homepage and click on it.
- Check the find checkbox.
- Click Save.
Setting roles and permissions for the Public role for the Bookshelf content type
Create users with roles
Strapi allows different roles for different access control for users to access content data. It allows a Public role for public access and an Authenticated role for authenticated access.
Let’s create two users: Bob and Alice. Bob will have a Public role and Alice will have an Authenticated role.
Start the app in a terminal window by npm run develop
. To create the two new registered users, we will make a curl
calls from a new terminal window.
Let’s create a new registered user Bob:
curl -X POST 'http://localhost:1337/api/auth/local/register' \
--header 'Content-Type: application/json' \
-d '{
"username": "bob",
"email": "bob@test.com",
"password": "Bob@public"
}'
The response to the above call:
{
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....pvTuY",
"user": {
"id": 1,
"username": "bob",
"email": "bob@test.com",
"provider": "local",
"confirmed": true,
"blocked": false,
"createdAt": "2023-02-12T15:41:59.403Z",
"updatedAt": "2023-02-12T15:41:59.403Z"
}
}
Note: By default, the register
API creates a user with an Authenticated role. To update the role to a Public role, we will have to use the Strapi Dashboard Admin UI by following steps below:
- Go to Content Manager -> Collection Types -> User.
- Click on the user
bob
. - Notice that under role, the role is defaulted to Authenticated.
- Under the role, click on the Add relation dropdown and select Public. The Authenticated role is removed.
- Click Save.
Similarly, let’s create a new registered user for Alice:
curl -X POST 'http://localhost:1337/api/auth/local/register' \
--header 'Content-Type: application/json' \
-d '{
"username": "alice",
"email": "alice@test.com",
"password": "Alice@auth"
}'
The response to the above call:
{
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....0fyyw",
"user": {
"id": 2,
"username": "alice",
"email": "alice@test.com",
"provider": "local",
"confirmed": true,
"blocked": false,
"createdAt": "2023-02-12T15:58:51.670Z",
"updatedAt": "2023-02-12T15:58:51.670Z"
}
}
You can verify that the two users got created by going to the Strapi Dashboard Admin UI.
Registered users Bob and Alice created
To get in-depth details of using the Strapi Dashboard Admin UI to manage content, please check out my article on How to create a Strapi CMS app to manage content.
Managing and consuming the content via APIs
We will add content for the homepage and bookshelves via the APIs that Strapi automatically created for us. See Strapi’s REST API Endpoints documentation for details.
We will look at the authenticated APIs i.e. create
, update
, and delete
APIs, and then the public API i.e. get
API. See Strapi’s Manage Role Permissions documentation to understand how to make authenticated API calls.
Authentication
Before we make any authenticated API calls, we need to login
by passing the identifier
(can be username or email) and the password
of a registered user with Authenticated access. This operation has to be done once till the authentication session expires. The login
call will return a JWT Token which we will be using in our subsequent autheticated calls.
Start the app in a terminal window by npm run develop
. To login, we will make a curl
call from a new terminal window.
In our case, let’s use user Alice to login since this user has an Authenticated role.
curl -X POST http://localhost:1337/api/auth/local \
--header 'Content-Type: application/json' \
-d '{
"identifier":"alice@test.com",
"password":"Alice@auth"
}'
The response to the above call is successful:
{
"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....pursc",
"user": {
"id": 2,
"username": "alice",
"email": "alice@test.com",
"provider": "local",
"confirmed": true,
"blocked": false,
"createdAt": "2023-02-12T15:58:51.670Z",
"updatedAt": "2023-02-12T15:58:51.670Z"
}
}
From the above response, we need the jwt
token to be passed in the Authorization
header for the subsequent API calls as a Bearer
token. Any authentication failures return a 401 (unauthorized)
error.
Creating content entry for “Homepage”
Start the app in a terminal window by npm run develop
. To create a new Homepage entry, we will make a curl
call from a new terminal window.
Let’s try to create the Homepage entry without passing in the authentication token.
curl -X PUT http://localhost:1337/api/homepage \
--header 'Content-Type: application/json' \
-d '{
"data": {
"header": "My Bookcase",
"sub_header": "Books are your best friends...",
"content": "Fill this area with bookshelves with books.",
"footer": "Copyright (c) 2023 Rupak Ganguly"
}
}'
We get a 403 Forbidden
error as a response to the above call. That tells us that we cannot access the creation call publicly and it needs authentication.
{
"data": null,
"error": {
"status": 403,
"name": "ForbiddenError",
"message": "Forbidden",
"details": {}
}
}
Now, let’s create the Homepage entry by passing in the authentication token.
curl -X PUT http://localhost:1337/api/homepage \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your_jwt_token_here>' \
-d '{
"data": {
"header": "My Bookcase",
"sub_header": "Books are your best friends...",
"content": "Fill this area with bookshelves with books.",
"footer": "Copyright (c) 2023 Rupak Ganguly"
}
}'
The response from the above call is successful:
{
"data": {
"id": 1,
"attributes": {
"header": "My Bookcase",
"sub_header": "Books are your best friends...",
"content": "Fill this area with bookshelves with books.",
"footer": "Copyright (c) 2023 Rupak Ganguly",
"createdAt": "2023-02-12T17:42:49.321Z",
"updatedAt": "2023-02-12T17:42:49.321Z",
"publishedAt": "2023-02-12T17:42:49.239Z"
}
},
"meta": {}
}
Consuming content for “Homepage”
Let’s make a curl
call to see if we can retrieve the Homepage content that we just created. Since we allowed retrieval calls (find and findOne) to be publicly accessible, we should not require an authenticated call.
curl -X GET http://localhost:1337/api/homepage
We get a successful response which is identical to the response from create call we made earlier. It also means that the roles we set are working properly.
Creating content entries for “Bookshelf”
Similar to Homepage, let’s create a couple of bookshelf entries.
curl -X POST http://localhost:1337/api/bookshelves \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your_jwt_token_here>' \
-d '{
"data": {
"name": "Business Books",
"description": "A collection of business books."
}
}'
curl -X POST http://localhost:1337/api/bookshelves \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your_jwt_token_here>' \
-d '{
"data": {
"name": "Technical Books",
"description": "A collection of technical books."
}
}'
Consuming content for “Bookshelf”
Let’s make a curl
call to see if we can retrieve the Bookshelf content that we just created. Since we allowed retrieval calls (find and findOne) to be publicly accessible, we should not require an authenticated call.
curl -X GET http://localhost:1337/api/bookshelves
The successfull response for the above call:
{
"data": [{
"id": 1,
"attributes": {
"name": "Business Books",
"description": "A collection of business books.",
"createdAt": "2023-02-12T18:05:31.168Z",
"updatedAt": "2023-02-12T18:05:31.168Z",
"publishedAt": "2023-02-12T18:05:31.160Z"
}
}, {
"id": 2,
"attributes": {
"name": "Technical Books",
"description": "A collection of technical books.",
"createdAt": "2023-02-12T18:06:04.937Z",
"updatedAt": "2023-02-12T18:06:04.937Z",
"publishedAt": "2023-02-12T18:06:04.934Z"
}
}],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 2
}
}
}
Updating relations
Last but not the least, we need to associate the Homepage with the two bookshelves that we created. To do that we will make an update call to the Homepage entry and pass it the relation
data for bookshelves. See Strapi’s Managing relations through the REST API documentation for details.
curl -X PUT http://localhost:1337/api/homepage \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your_jwt_token_here>' \
-d '{
"data": {
"bookshelves": {
"connect": [
{ "id": 1 },
{ "id": 2 }
],
}
}
}'
Note: This above call does not work. Or at least I could not get it to work.
For that reason, I used the Strapi Dashboard Admin UI to associate the bookshelves to the homepage.
Associating the bookshelves to the homepage
Consuming data including relations
Let’s verify that the relations have been set properly:
curl -X GET http://localhost:1337/api/homepage?populate=*
The response was successful and you will notice that the single call to the homepage retrieves the associated bookshelves data as well:
{
"data": {
"id": 1,
"attributes": {
"header": "My Bookcase",
// ... clipped for brevity
},
"bookshelves": {
"data": [{
"id": 1,
"attributes": {
"name": "Business Books",
// ... clipped for brevity
}
}, {
"id": 2,
"attributes": {
"name": "Technical Books",
// ... clipped for brevity
}
}]
}
}
},
"meta": {}
}
Similar to the creation and retrieval calls, we can also make delete calls. This concludes the section for managing content for the content types that we defined.
To get in-depth details of using the Strapi Dashboard Admin UI to manage content, please check out my article on How to create a Strapi CMS app to manage content
That concludes the article looking at Strapi CMS for programmatically creating content types, managing content, and consuming the content via the Strapi CLI and APIs. You can follow along with the article and review the source code repository for the strapi-bookshelf
app project we created in this article.
Summary
To recap, we learned how to create a new Strapi app bookshelf using the Strapi CLI. We created a new admin user to manage content types using the Strapi CLI. We then explored the anatomy of the Strapi app we created by reviewing the project structure, files, and auto-generated code. We then learned how to programmatically generate a single and a collection of content types using the Strapi CLI. We also learned how to update the content schema to add other extra constraints and relations. We learned how to limit access controls for exposing authenticated and public operations by setting roles and permissions. Next, we learned how to create a couple of end users with authenticated and public roles to consume the content. We also learned how to create, update and query content with relations programmatically using the APIs.
If you have questions or feedback, please let me know in the comments below.