How to Customize the Strapi Back-end Using TypeScript

How to Customize the Strapi Back-end Using TypeScript

In this article, we'll examine the various systems (services, controllers, and routes) that Strapi has using TypeScript.

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:

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 localhost:1337/admin.

Building our Application Basics (Modelling Data and Content Types)

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).

relationship between articles and users

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.

  1. In your terminal, run yarn strapi generate or npm run generate.
  2. Select content-type.
  3. Name the content-type category.
  4. Under "Choose the model type," select Collection Types.
  5. Do not add a draft and publish system.
  6. Add name as text attribbute.
  7. Proceed and select add model to new API.
  8. Name the API category.
  9. 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:

  1. In the Strapi admin dashboard, navigate to the article content-type.
  2. Add a new Relation field as follows (see image below).
  3. Click save.

relationship between category and article

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.

  1. On the side menu, select settings.
  2. Under users & permissions plugins, select roles.
  3. Click on public.
  4. Under 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.
  5. 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.

  1. 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.

  2. 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

  1. Navigate to the src/api/article/services/article.ts file.
  2. 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.

  3. 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.

  4. 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.

  1. 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
  1. 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.

API request to create an article

Open the admin dashboard and view the article you just created.

Article collection type

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.

  1. Navigate to the src/api/article/controllers/article.ts file
  2. 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:

  1. Configuring Core Routers: Enables the extension of the default Strapi routers functionality.
  2. 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

  1. Configuring Core Routers: A core router file is a Javascript file exporting the result of a call to createCoreRouter with some parameters. Open up your src/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.

  2. 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:

  1. The 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>.
  2. Hence, the code above will cause Strapi to throw an error and exit because we do not have a getSlugs method in our article controller. We will create the getSlugs method soon.
  3. 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.

  1. Return to the 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.
  2. Open your src/api/article/controllers/article.ts file. Add the following lines of code to it - just below the find 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.

  1. 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.