Author: Alex Godwin
Every back-end service in existence has some form of system in place that enables them to receive requests and act on them appropriately (providing some response). Services, routes, and controllers are all represented in such servers. Strapi also has this system in place and offers ways for you to customize them to your preferences, such as adding capabilities or developing new ones.
In this article, using TypeScript, we'll examine the various systems (services, controllers, and routes) that Strapi has in place to receive and respond to requests. You'll learn how to customize these systems since, as it turns out, most times, the default settings are frequently insufficient for achieving your goals; therefore, knowing how to do so is highly useful.
What You'll Build
In this tutorial, you'll learn how to create an API for articles, which can be queried by either an API client or an application’s front-end. To better understand the internals of Strapi, you’ll have to build this from scratch and add the features.
Prerequisites
Before continuing in this article, ensure you have the following:
- Knowledge of JavaScript,
- Node.js (v14 recommended for Strapi), and
- Access to an API client (e.g Postman or Insomnia REST)
Introduction to Strapi
Strapi is the leading open-source, customizable, headless CMS based on JavaScript that allows developers to choose their favorite tools and frameworks while also allowing editors to manage and distribute their content easily.
Strapi enables the world's largest companies to accelerate content delivery while building beautiful digital experiences by making the admin panel and API extensible through a plugin system.
Getting started with Strapi using TypeScript
To install Strapi, head over to the Strapi documentation. We’ll be using the SQLite database for this project.
To install Strapi with TypeScript support, run the following commands:
npx create-strapi-app my-app --ts
Replace my-app
with the name you wish to call your application directory. Your package manager will create a directory with the specified name and install Strapi. If you have followed the instructions correctly, you should have Strapi installed on your machine.
Run the following commands to start the Strapi development server:
yarn develop # using yarn
npm run develop # using npm
The development server starts the app on http://localhost:1337/admin.
Building our Application Basics (Modelling Data and Content Types)
As part of the first steps, follow the instructions below:
- Open up the Strapi admin dashboard in your preferred browser.
- On the side menu bar, select
Content-Type Builder
. - Select
create new collection type
. - Give it a display name,
article
. - Create the following fields:
a.
title
as short text b.slug
as short text c.content
as rich text d.description
as long text e. Create a relationship between articles and user (users_permissions_user).
Using the "Strapi generate" Command
You just created an article
content type; you can also create content types using the strapi generate
command. Follow the instructions below to generate a category content-type using the strapi generate
command.
- In your terminal, run
yarn strapi generate
ornpm run generate
. - Select
content-type
. - Name the content-type
category
. - Under "Choose the model type," select
Collection Types
. - Do not add a draft and publish system.
- Add name as
text
attribbute. - Proceed and select
add model to new API
. - Name the API category.
- Select
yes
to Bootstrap API related files.
To verify, open up the Strapi admin dashboard in your preferred browser. You should see that a category content type has been created.
Next, it's time to create a relationship between the category and the article content-types:
- In the Strapi admin dashboard, navigate to the article content-type.
- Add a new
Relation field
as follows (see image below). - Click save.
You now have a base API alongside all the necessary content types and relationships.
Permissions
Since this article focuses on services, routes, controllers, and queries, the next phase involves opening up access to the user, article, and category content type to public requests.
- On the side menu, select
settings
. - Under
users & permissions plugins
, selectroles
. - Click on
public
. - Under
permissions
, a. ClickArticles
, and check "select all". b. ClickCategory
and check "select all". c. Clickusers-permission
, scroll to "users" and check the "select all" box. - Click save.
Now, all content-type activities are available to public requests; this would allow us to make requests without having to get JWTs. Finally, create a user with a public role and create some categories.
Generate Typings
To allow us to use the correct types in our projects, you must generate TypeScript typings for the project schemas.
- Run the following command in your terminal:
yarn strapi ts:generate-types //using yarn
or
npm run strapi ts:generate-types //using npm
In the project's root folder, you should notice that a schema.d.ts
file has been created. You may note when you browse the file that it contains type definitions for each of the project's content-types.
- We also need the general types; copy the code from this link GitHub into a
general-schemas.d.ts
file that you’ll create in the project’s root folder.
Introduction to Strapi Services
Services are reusable functions that typically carry out specialized activities; however, a collection of services may work together to carry out more extensive tasks.
Services should house the core functionality of an application, such as API calls, database queries, and other operations. They help to break down the logic in controllers.
Customizing Strapi Services
Let's look at how to modify Strapi's services. To begin, open the Strapi backend in your preferred code editor.
Modifying the "Article" Services
- Navigate to the
src/api/article/services/article.ts
file. - Replace its content with the following lines of code:
import { factories } from '@strapi/strapi';
import schemas from '../../../../schemas'
import content_schemas from '../../../../general-schemas';
export default factories.createCoreService('api::article.article', ({ strapi }): {} => ({
async create(params: { data: content_schemas.GetAttributesValues<'api::article.article'>, files: content_schemas.GetAttributesValues<'plugin::upload.file'> }): Promise<schemas.ApiArticleArticle> {
params.data.publishedAt = Date.now().toString()
const results = await strapi.entityService.create('api::article.article', {
data: params.data,
})
return results
},
}))
The block of code above modifies the default Strapi create()
service.
params.data.publishedAt
is set to the current time(i.eDate.now()
). Since we are using theDraftAndPublish
system, we want whatever is being created through the API to be published immediately.strapi.entityService.create()
is being called to write data to the database; we’ll learn more aboutentity services
in the next sub-section.
Let’s add a service that’ll help us with slug creation (if you look at the article content type, you’ll notice that we have a slug field). It’d be nice to have slugs auto-generated based on the title of an article.
- You'll need to install two npm packages: slugify and randomstring. In your terminal, run the following commands:
yarn add slugify randomstring //using yarn
or
npm install slugify randomstring //using npm
- Update the content of
src/api/article/services/article.ts
with the following code:
import { factories } from '@strapi/strapi';
import slugify from 'slugify';
import schemas from '../../../../schemas';
import content_schemas from '../../../../general-schemas';
import randomstring from 'randomstring';
export default factories.createCoreService('api::article.article', ({ strapi }): {} => ({
async create(params: { data: content_schemas.GetAttributesValues<'api::article.article'>, files: content_schemas.GetAttributesValues<'plugin::upload.file'> }): Promise<schemas.ApiArticleArticle> {
params.data.publishedAt = Date.now().toString()
params.data.slug = await this.slug(params.data.title)
const results = await strapi.entityService.create('api::article.article', {
data: params.data,
})
return results
},
async slug(title: string): Promise<string> {
const entry: Promise<schemas.ApiArticleArticle> = await strapi.db.query('api::article.article').findOne({
select: ['title'],
where: { title },
});
let random = entry == null ? '' : randomstring.generate({
length: 6,
charset: 'alphanumeric'
})
return slugify(`${title} ${random}`, {
lower: true,
})
}
}));
The code above is to add a slugify
function to our services. Pay close attention to the code, and you’ll notice the use of strapi.db.query().findOne()
. This is a concept called Queries Engine API
. Alongside entity services
, Queries
are a means to interact with the database.
Let’s test our services to see that they work fine. Open up your API client and make a POST
request to the following route http://localhost:1337/api/articles
.
Open the admin dashboard and view the article you just created.
If you try to create entries with the same title, you’ll notice that the duplicate entries have different slugs.
Entity Services and Query API
Entity services and queries both allow us to interact with the database. However, Strapi recommends using the Entity service API whenever possible as it is an abstraction around the Queries API, which is more low-level. The Strapi documentation gives accurate information on when to use one or the other, but I’ll go over it a bit.
The Entity service API provides a couple of methods for CRUD operations, i.e. (findOne()
, create()
, findMany()
, update()
, and delete()
). However, it could fall short more frequently than not as it lacks the flexibility of the Query API. For instance, using a where
clause with Entity services is not possible, whereas doing so with the Query API is. The distinction between the findOne()
methods used by the Query APIs and the Entity service provides another striking illustration. The Query API allows us to specify the condition using a where clause, whereas the Entity service only allows us to use findOne()
with an id
.
Introduction to Strapi Controllers
Controllers are JavaScript files with a list of actions the client can access based on the specified route. The C in the model-view-controller (MVC) pattern is represented by the controller. Services are invoked by controllers to carry out the logic necessary to implement the functionality of the routes that a client requests.
Customizing Strapi Controllers
Let's examine how to alter Strapi controllers. Start by opening the Strapi backend in your favorite code editor.
- Navigate to the
src/api/article/controllers/article.ts
file - Replace its content with the following lines of code:
/**
* article controller
*/
import { factories } from '@strapi/strapi'
import schemas from '../../../../schemas'
import content_schemas from '../../../../general-schemas';
export default factories.createCoreController('api::article.article', ({ strapi }): {} => ({
async find(ctx: any): Promise<content_schemas.ResponseCollection<'api::article.article'>> {
return await super.find(ctx)
}
}));
We are modifying the default find()
controller, although it still has the same functionality because the super.find()
method is the default action for the find controller. The ctx
object contains data about the request from the client, e.g. ctx.request
, ctx.query
, ctx.params
. We’ll see more about controllers soon enough.
Strapi Routes
Routes handle all requests that are sent to Strapi. Strapi automatically creates routes for all content-types by default. Routes can be specified and added.
Strapi allows two (2) different router file structures:
- Configuring Core Routers: Enables the extension of the default Strapi routers functionality.
- Creating Custom Routers: Allows to develop completely new routes.
Strapi provides different params to go with the different router file structures. Let’s see how to work with both types of routers.
Customizing Strapi Routes
- Configuring Core Routers: A core router file is a Javascript file exporting the result of a call to
createCoreRouter
with some parameters. Open up yoursrc/api/article/routes/article.ts
file and update it’s contents with the following:
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::article.article', {
only: ['find'],
config: {
find: {
auth: false,
policies: [],
middlewares: [],
}
}
});
The only
array signifies what routes to load; anything not in this array is ignored. No authentication is required when auth is set to false, making such a route accessible to everyone.
- Creating Custom Routers: To use a custom router, we must create a file that exports an array of objects, each of which is a route with a set of parameters.
Naming Convention
Routes files are loaded in alphabetical order. To load custom routes before core routes, make sure to name custom routes appropriately (e.g. 01-custom-routes.js
and 02-core-routes.js
). Create a file src/api/article/routes/01-custom-article.ts
, then fill it up with the following lines of code:
export default {
routes: [
{
// Path defined with an URL parameter
method: 'GET',
path: '/articles/single/:slug',
handler: 'article.getSlugs',
config: {
auth: false
}
},
]
}
Below are some things to note from the above snippet of code:
- The
handler
parameter allows us to specify which controller we would like to use in handling requests sent to thepath
. it takes the syntax of<controllerName>.<actionName>
. - Hence, the code above will cause Strapi to throw an error and exit because we do not have a
getSlugs
method in ourarticle controller
. We will create thegetSlugs
method soon. - We have auth set to false, which means that this route is available for public requests.
Case Study: Building a Slug Route
In this case study, a route, together with its controllers and services, will be built. The route enables us to retrieve an article using its slug.
- Return to the
01-custom-article.ts
file; it’s already set, what you’ll have to do now is build thegetSlugs
action for thearticle controller
. - Open your
src/api/article/controllers/article.ts
file. Add the following lines of code to it - just below thefind
method.
//... other actions
async getSlugs(ctx: any): Promise<schemas.ApiArticleArticle['attributes']> {
const data = {
params: ctx.params,
query: ctx.query
}
let response = await strapi.service('api::article.article').getSlugs(data)
delete response.users_permissions_user.password
delete response.users_permissions_user.resetPasswordToken
delete response.users_permissions_user.confirmationToken
return response
}
//... other actions
In the code snippet above, ctx.params
contain the dynamic parameters from the route and ctx.query
contains all additional query params. We pass an object made up of both ctx.params and ctx.query
to the getSlugs()
service.
- Open your
src/api/article/services/article.ts
file and copy the lines of code below into it - just below the slugs method.
//... other services
async getSlugs(params: { params: any, query: any }): Promise<schemas.ApiArticleArticle> {
if(params.query.populate == '*') {
params.query.populate = [ 'category', 'users_permissions_user' ]
} else if(params.query.populate != undefined) {
params.query.populate = [ params.query.populate ]
}
const data: Promise<schemas.ApiArticleArticle['attributes']> = await strapi.db.query('api::article.article').findOne({
where: { slug: params.params.slug },
populate: params.query.populate || []
})
delete data.users_permissions_user.password
return data
}
//... other services
Finally, it's time to create the getSlugs
service. Using the Query API
, search for an article that has the same slugs as that given to the params. We'll also populate the data depending on the query params.
Open up your API client and make a GET request to the following URL http://localhost:1337/api/articles/single/${slug}?populate=*
here ${slug}
represents a valid slug from your database.
Conclusion
You've broken out the services, controllers, and routes of the Strapi in this article. You have written TypeScript code that demonstrates how to edit and build these internal processes from the ground up. You learned how to generate appropriate types. Now that you know more about what's happening in your Strapi backend, hopefully, you can approach it with more confidence going forward.