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.
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.
Before continuing in this article, ensure you have the following:
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.
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.
As part of the first steps, follow the instructions below:
1. Open up the Strapi admin dashboard in your preferred browser.
2. On the side menu bar, select Content-Type Builder
.
3. Select create new collection type
.
4. Give it a display name, article
.
5. 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).
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.
yarn strapi generate
or npm run generate
.content-type
.category
.Collection Types
.text
attribbute.add model to new API
.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:
Relation field
as follows (see image below).You now have a base API alongside all the necessary content types and relationships.
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.
settings
.users & permissions plugins
, select roles
.public
.permissions
,
a. Click Articles
, and check "select all".
b. Click Category
and check "select all".
c. Click users-permission
, scroll to "users" and check the "select all" box.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.
To allow us to use the correct types in our projects, you must generate TypeScript typings for the project schemas.
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.
general-schemas.d.ts
file that you’ll create in the project’s root folder.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.
Let's look at how to modify Strapi's services. To begin, open the Strapi backend in your preferred code editor.
src/api/article/services/article.ts
file.1 import { factories } from '@strapi/strapi';
2 import schemas from '../../../../schemas'
3 import content_schemas from '../../../../general-schemas';
4
5 export default factories.createCoreService('api::article.article', ({ strapi }): {} => ({
6
7 async create(params: { data: content_schemas.GetAttributesValues<'api::article.article'>, files: content_schemas.GetAttributesValues<'plugin::upload.file'> }): Promise<schemas.ApiArticleArticle> {
8 params.data.publishedAt = Date.now().toString()
9 const results = await strapi.entityService.create('api::article.article', {
10 data: params.data,
11 })
12 return results
13 },
14 }))
The block of code above modifies the default Strapi create()
service.
params.data.publishedAt
is set to the current time(i.e Date.now()
). Since we are using the DraftAndPublish
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 about entity 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.
yarn add slugify randomstring //using yarn
or
npm install slugify randomstring //using npm
src/api/article/services/article.ts
with the following code:1 import { factories } from '@strapi/strapi';
2 import slugify from 'slugify';
3 import schemas from '../../../../schemas';
4 import content_schemas from '../../../../general-schemas';
5 import randomstring from 'randomstring';
6
7 export default factories.createCoreService('api::article.article', ({ strapi }): {} => ({
8
9 async create(params: { data: content_schemas.GetAttributesValues<'api::article.article'>, files: content_schemas.GetAttributesValues<'plugin::upload.file'> }): Promise<schemas.ApiArticleArticle> {
10 params.data.publishedAt = Date.now().toString()
11 params.data.slug = await this.slug(params.data.title)
12 const results = await strapi.entityService.create('api::article.article', {
13 data: params.data,
14 })
15 return results
16 },
17
18 async slug(title: string): Promise<string> {
19 const entry: Promise<schemas.ApiArticleArticle> = await strapi.db.query('api::article.article').findOne({
20 select: ['title'],
21 where: { title },
22 });
23 let random = entry == null ? '' : randomstring.generate({
24 length: 6,
25 charset: 'alphanumeric'
26 })
27 return slugify(`${title} ${random}`, {
28 lower: true,
29 })
30 }
31 }));
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 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
.
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.
Let's examine how to alter Strapi controllers. Start by opening the Strapi backend in your favorite code editor.
src/api/article/controllers/article.ts
file1 /**
2 * article controller
3 */
4 import { factories } from '@strapi/strapi'
5 import schemas from '../../../../schemas'
6 import content_schemas from '../../../../general-schemas';
7
8 export default factories.createCoreController('api::article.article', ({ strapi }): {} => ({
9
10 async find(ctx: any): Promise<content_schemas.ResponseCollection<'api::article.article'>> {
11 return await super.find(ctx)
12 }
13 }));
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.
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:
Strapi provides different params to go with the different router file structures. Let’s see how to work with both types of routers.
createCoreRouter
with some parameters. Open up your src/api/article/routes/article.ts
file and update it’s contents with the following:1 import { factories } from '@strapi/strapi';
2
3 export default factories.createCoreRouter('api::article.article', {
4 only: ['find'],
5 config: {
6 find: {
7 auth: false,
8 policies: [],
9 middlewares: [],
10 }
11 }
12 });
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.
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:
1 export default {
2 routes: [
3 {
4 // Path defined with an URL parameter
5 method: 'GET',
6 path: '/articles/single/:slug',
7 handler: 'article.getSlugs',
8 config: {
9 auth: false
10 }
11 },
12 ]
13 }
Below are some things to note from the above snippet of code:
handler
parameter allows us to specify which controller we would like to use in handling requests sent to the path
. it takes the syntax of <controllerName>.<actionName>
.getSlugs
method in our article controller
. We will create the getSlugs
method soon.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.
01-custom-article.ts
file; it’s already set, what you’ll have to do now is build the getSlugs
action for the article controller
.src/api/article/controllers/article.ts
file. Add the following lines of code to it - just below the find
method.1 //... other actions
2 async getSlugs(ctx: any): Promise<schemas.ApiArticleArticle['attributes']> {
3 const data = {
4 params: ctx.params,
5 query: ctx.query
6 }
7 let response = await strapi.service('api::article.article').getSlugs(data)
8 delete response.users_permissions_user.password
9 delete response.users_permissions_user.resetPasswordToken
10 delete response.users_permissions_user.confirmationToken
11 return response
12 }
13 //... 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.
src/api/article/services/article.ts
file and copy the lines of code below into it - just below the slugs method.1 //... other services
2 async getSlugs(params: { params: any, query: any }): Promise<schemas.ApiArticleArticle> {
3
4 if(params.query.populate == '*') {
5 params.query.populate = [ 'category', 'users_permissions_user' ]
6 } else if(params.query.populate != undefined) {
7 params.query.populate = [ params.query.populate ]
8 }
9
10 const data: Promise<schemas.ApiArticleArticle['attributes']> = await strapi.db.query('api::article.article').findOne({
11 where: { slug: params.params.slug },
12 populate: params.query.populate || []
13 })
14
15 delete data.users_permissions_user.password
16 return data
17 }
18 //... 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.
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.
Alexander Godwin is a Software Developer and writer that likes to write code and build things. Learning by doing is the best way and it's how Alex helps others learn. Follow him on Twitter (@oviecodes)