Building a Plugin with TypeScript
Tags: strapi, headlesscms, typescript, plugins
Published: 2022-08-29 08:20:00

_This article accurately explains how to create a plugin in Strapi using TypeScript._

**Author: Alex Godwin**

Strapi recently made available a version that enables typescript development. Finally, we have type safety in the CMS that we all love. You can [read about it here](https://forum.strapi.io/t/strapi-v4-2-0-beta-with-typescript-support-dark-mode/16732).

The ability to customize computer programs, mobile apps, and online browsers is made possible via plugins, which are software add-ons. Plugins let us add to software functionality that the product's author did not provide by default. Strapi enables us to create plugins and increase the functionality of the CMS.

In this article, we'll learn how to create a Strapi plugin in TypeScript that lets us select fields from our models to which we want to apply slugs. So, we'll create a plugin that enables us to have a slug system.

## Prerequisites
To follow this article, you'll need:
- Basic Knowledge of [React.js](https://reactjs.org/),
- Knowledge of JavaScript, and
- [Node.js](https://nodejs.org/) (v14 recommended for Strapi).


**The completed version of your application should look like the images below:**

![Homepage of our plugin](https://paper-attachments.dropbox.com/s_3885D325D61DA0D2924A7E5C3720C5214F47A72B1E63E50844530DB952C69523_1659343839203_Screenshot+from+2022-08-01+09-44-00.png)

![Plugin injected a slug property on our model](https://paper-attachments.dropbox.com/s_3885D325D61DA0D2924A7E5C3720C5214F47A72B1E63E50844530DB952C69523_1659344080973_Screenshot+from+2022-08-01+09-46-29.png)



![Slug is automatically generated once data is saved](https://paper-attachments.dropbox.com/s_3885D325D61DA0D2924A7E5C3720C5214F47A72B1E63E50844530DB952C69523_1659344262146_Screenshot+from+2022-08-01+09-48-51.png)

## An Introduction to Strapi
The [Strapi documentation](https://strapi.io/documentation/developer-docs/latest/getting-started/introduction.html) says that "[Strapi is a flexible, open-source Headless CMS](https://strapi.io) that gives developers the freedom 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.

## Scaffolding a Strapi Project with TypeScript

[To install Strapi, head over to the Strapi docs ](https://docs.strapi.io/developer-docs/latest/getting-started/introduction.html). We’ll be using the SQLite database for this project. To install Strapi with Typescript support, run the following commands:

```bash
    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:

```bash
    yarn develop # using yarn
    npm run develop # using npm
```

The development server starts the app on http://localhost:1337/admin. 


## Creating a Strapi Plugin

Creating a Strapi plugin is easy. Run the following command:

```bash
    npm run strapi generate
    or
    yarn strapi generate
```

From the options, choose plugin. Name the plugin `slugify` and choose TypeScript as the language of choice. A section of code resembling the following will appear in your terminal after performing the steps above,

```js
    export default {
        'slugify': {
            enabled: true,
            resolve: './src/plugins/slugify'
        }
    }
```

Create a file called `config/plugins.ts` and add the code stated before to it.

To start using our plugin, the following actions must be taken:


1. Run `npm run install` or `yarn install` in the newly-created plugin directory, i.e `src/plugins/slugify`.
2. Run `yarn build` or `npm run build` to build the plugin. Every time we make a modification to our plugin, we run this command.

Finally, run the following command in order to start our server in watch mode.
```bash
    yarn strapi develop --watch-admin
```

## Admin Server Development
In this section, we’ll build the server side of our plugin and learn a couple of new Strapi concepts along the way.

### Routes

Whenever we create an API, we start with the route. Strapi automatically generates the following route:

```js
    // server/routes/index.ts
    export default [
      {
        method: 'GET',
        path: '/',
        handler: 'myController.index',
        config: {
          policies: [],
        },
      },
    ];
```

It would be convenient to have a route in our application that fetches the content types that are accessible (i.e. content-types that are visible to the API). Since our plugin, Slugify, allows users to select which fields to apply slugs to, we would also like to change certain content-types based on user input; building a route for that would be helpful.

```js
    export default [
      {
        method: 'GET',
        path: '/',
        handler: 'slugController.index',
        config: {
          policies: [],
          auth: false,
        },
      },
      {
        method: 'GET',
        path: '/allContentTypes',
        handler: 'slugController.getContentTypes',
        config: {
          policies: [],
          auth: false,
        }
      },
      {
        method: 'POST',
        path: '/setSlugs',
        handler: 'slugController.setSlugs',
        config: {
          policies: [],
          auth: false,
        }
      }
    ];
```

By setting `auth: false`, we make the route publicly accessible. If you visit `http://localhost:1337/slugify`, you should get a response that says "Welcome to Strapi 🚀." That’s the default response from the index route.


### Controllers
Controllers which are referenced in the handler property of our routes allow us to add actions to routes. We have a default `slugify/server/controllers/my-controller.ts` file.  We’ll rename that file to  `slugify/server/controllers/slugController.ts`. Then, we'll add the following lines of code.

```js
    import '@strapi/strapi';
    export default ({ strapi }: { strapi: any }) => ({
      index(ctx: any) {
        ctx.body = strapi
          .plugin('slugify')
          .service('slugService')
          .getWelcomeMessage();
      },
    
      async getContentTypes(ctx: any) {
        try {
          ctx.body = await strapi
            .plugin('slugify')
            .service('slugService')
            .getContentTypes();
        } catch (err) {
          ctx.throw(500, err);
        }
      },
    
      async setSlugs(ctx: any) {
        const { body, headers } = ctx.request;
        // console.log(headers)
        try {
          await strapi
            .plugin('slugify')
            .service('slugService')
            .setSlugs(body, headers)
          ctx.body = await strapi
          .plugin('slugify')
          .service('slugService')
          .getContentTypes();
          
        } catch (err) {
          ctx.throw(500, err);
        }
        
      }
      
    });
```
Notice how the methods in our slugController.ts file match the ones in each routes handler.

Finally, let's change the content of the `slugify/server/controller/index.ts` file to the following:

```js
    import slugController from './slugController';
    
    export default {
      slugController,
    };
```

### Services
Our controllers above show us that specific `slugService` service properties are being called. Let's start by making the service file and the associated functions.

- Rename the `slugify/server/services/my-service.ts` to `slugService.ts`
- Change the content of the `slugify/server/services/index.ts` file to the following:
```js
    import slugService from './slugService';
    
    export default {
      slugService,
    };
```

Let’s take a second to understand what our function in `slugify/server/services/slugService.ts` will do: 


- The `getContentTypes` function allows us to fetch the available `content-types` from our Strapi application. To accomplish this, we will use the built-in Strapi method (`strapi.contentTypes`), which returns all content-types within our application (such as plugins and APIs), and then apply some logic to the response to get the desired outcomes.

In `slugify/server/services/slugService.ts`, add the following lines of code:

```js
    import '@strapi/strapi';
    
    export default ({ strapi }: { strapi: any }) => ({
      
      getWelcomeMessage() {
        return 'Welcome to Strapi 🚀';
      },
      
      async getContentTypes() {
        const contentTypes = strapi.contentTypes
        return Object.values(contentTypes).filter((el: any) => el.uid.includes('api::'))
      },
      
    });
```

Visit `http://localhost:1337/allContentTypes` you’ll get a JSON response that contains all available `content-types`. If you don’t have any `content-types` then go ahead and create some.


## Using the Content-Type Plugin Programmatically
The `setSlugs` function will enable us to modify the available `content-types`. In order to do this we’ll have to access the `content-type` plugin programmatically using the `strapi.plugin('content-type-builder').controller('content-types').updateContentType(ctx)`.

1. Add the following code to the default export of your `slugify/server/services/slugService.ts` file:

```js
    //... other methods 
    async setSlugs(ctx: any, headers:any) {
        let { pluginOptions, info, collectionName, options, attributes, kind  } = ctx
        const toDelete = [ 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', 'publishedAt', 'slug' ]
        toDelete.map((attr, i) => {
          delete attributes[attr]
        })
        if(ctx.slugEnabled && ctx.slugField) {
          pluginOptions = {
            slug: {
              field: ctx.slugField
            }
          }
        } else {
          pluginOptions = {}
        }
       
        const data: any =  {
          pluginOptions,
          collectionName,
          draftAndPublish: options.draftAndPublish,
          singularName: info.singularName,
          pluralName: info.pluralName,
          attributes,
          displayName: info. displayName,
          kind,
          description: info.description
        }
        
        ctx.request = {
          body: {
            contentType: data,
            components: [] 
          }
        }
        ctx.params = { uid: ctx.uid }
        try {
          strapi.plugin('content-type-builder').controller('content-types').updateContentType(ctx);
        } catch(e) {
          console.log('service', e.errors)
        }
        return
      },
    
    //... other methods
```
We’ll create a method that allows us apply slugs to the selected field. Since the API won't allow access to this function, neither a route nor a controller are created for it.

[Slugify](https://www.npmjs.com/package/slugify), an npm package, will be needed in order to make this service. To install the Slugify package, run:

```bash
    npm i slugify
    or
    yarn add slugify
```

2. Add the following import statement to your `slugify/server/services/slugService.ts` file:
```js
    //... strapi imports 
    import slugify from 'slugify'
```

3. Finally, add the following lines of code to your `slugify/server/services/slugService.ts` file:

```js
    // ... other methods
    
    slugify(ctx: any, field:any) {
        return slugify(ctx[field], {
          lower: true
        })
        
      }
    
    // ... other methods
```


## Lifecycle Functions: register(), bootstrap(), and destroy()
Lifecycle functions represents the different phases of plugin integration. You can read about the server-side lifecycle functions of plugins on the [Strapi documentation](https://documentation-git-dev-typescript-typings-strapijs.vercel.app/developer-docs/latest/developer-resources/plugin-api-reference/server.html#entry-file).

We’ll be using the `register()` lifecycle function to add the `slug` attribute to the selected `content-types`.

In your `src/plugins/slugify/server/register.ts` file, add the following code:

```js
    export default ({ strapi }: { strapi: any}) => {
      // registeration phase
      Object.values(strapi.contentTypes).forEach(async (contentType: any) => {
        
        if (contentType.uid.includes("api::") && !!contentType.pluginOptions.slug) {
         
          // Add tasks property to the content-type
          contentType.attributes.slug = {
            type: "string",
            default: `${contentType.pluginOptions.slug.field}-slug`,
            configurable: false,
          };
    
        }
      });
    };
```
Next, we want to set up a system that enables us to watch as a content-type is being created and updated. `Lifecycle hooks` give us the ability to accomplish that.


### Setting up Lifecycle Hooks
A complete list of the `Lifecycle hooks` that Strapi provides can be found [here](https://docs.strapi.io/developer-docs/latest/development/backend-customization/models.html#available-lifecycle-events); you could have a look at them. We’ll be using the `beforeCreate` and `beforeUpdate` hooks.

Update the content of your `src/plugins/slugify/server/register.ts` with the following code:

```js
    import "@strapi/strapi";
    import * as path from "node:path";
    import * as fsSync from "node:fs";
    
    export default ({ strapi }: { strapi: any }) => {
      // registeration phase
      Object.values(strapi.contentTypes).map(async (contentType: any) => {
        if (contentType.uid.includes("api::")) {
          const lifecycle = path.join(
            `${process.cwd()}/src/api/${contentType.apiName}/content-types/${contentType.apiName
            }/lifecycles.ts`
          );
          const fileExist = fsSync.readFileSync(lifecycle).toString().length != 0;
          if ((!contentType.pluginOptions.slug && fileExist)) {
            delete contentType.attributes.slug
            return
          }
          if (!contentType.pluginOptions.slug) {
            return
          }
          // Add tasks property to the content-type
          contentType.attributes.slug = {
            type: "string",
            default: `${contentType.pluginOptions.slug.field}-slug`,
            configurable: false,
          };
          if (fileExist) return;
          const data = `export default {
            beforeCreate(event: any) {
              const { data, where, select, populate } = event.params;
              const slugField = { field: '${contentType.pluginOptions.slug.field}' }
              if(slugField.field) event.params.data.slug = strapi.plugin('slugify').service('slugService').slugify(data, '${contentType.pluginOptions.slug.field}')
        
              return
            },
            beforeUpdate(event: any) {
              const { data, where, select, populate } = event.params;
              const slugField = { field: '${contentType.pluginOptions.slug.field}' }
              if(slugField.field) event.params.data.slug = strapi.plugin('slugify').service('slugService').slugify(data, '${contentType.pluginOptions.slug.field}')
              return
            }
          };`;
          // then create a lifecycle.ts file
          fsSync.writeFileSync(lifecycle, data);
    
        }
    
      });
    };
```    

In the code snippet above, we are programmatically creating a `lifecycle.ts` file for the `content-type` only if it has `slug` object in it’s `pluginOptions`. You can see the `lifecycle.ts` file we created in the `./src/api/[api-name]/content-types/[model-name]` directory.


### Restarting the Server Programmatically

After creating the `lifecycle.ts` file, we need a way to restart the server in order for it to know about the `Lifecycle hooks` we just set up.

To restart the server programmatically, add the following line of code to your `src/plugins/slugify/server/register.ts` file, just immediately after the `fsSync.writeFileSync(lifecycle, data);`  line

```js
    strapi.reload()
```

Every time we make changes to our plugin, we execute the following command in `src/plugins/slugify` to build the plugin.

```bash
    yarn build
    or 
    npm run build
```
That’s all for the server side of our `slugify` plugin. Next, we’ll see how to build the admin section of our plugin.


## Admin Client Development

We’ll have a simple Admin client which will enable users do two things:

1. Select a `content-type` that they’ll like to apply our plugin to.
2. Select a field which slugs will be created from; this can only be a `string`  field.

Let’s start developing our the client-side of our plugin.

### Building the Home Page
Let's begin by creating the home page. 

Navigate to `src/plugins/slugify/admin/src/pages/HomePage/index.tsx`, then update it’s content with the following lines of code:

```js
    import React, { Suspense, memo, useState, useEffect } from 'react';
    import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
    import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
    import { Illo } from '../../components/Illo';
    
    const HomePage: React.VoidFunctionComponent = () => {
      return (
          <>
              <BaseHeaderLayout
                title="Slugify Plugin"
                subtitle="Click checkbox to allow slugs on Content Type"
                as="h2"
              />
      
              <ContentLayout>
                {contentTypeCount === 0 && !loading && (
                  <EmptyStateLayout icon={<Illo />} content="You don't have any Content Types yet..." />
                )}
              </ContentLayout>
          </>
        );
    };
    
    export default memo(HomePage);
```

Let’s create the `Illo` component which is used for our empty state:

1. Create a directory `Illo` in the `src/plugins/slugify/admin/src/components` folder
2. Create a file `index.js` then add the following code to it

```js
    import React from 'react';
    export const Illo = () => (
      <svg width="159" height="88" viewBox="0 0 159 88" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path
          fillRule="evenodd"
          clipRule="evenodd"
          d="M134.933 17.417C137.768 17.417 140.067 19.7153 140.067 22.5503C140.067 25.3854 137.768 27.6837 134.933 27.6837H105.6C108.435 27.6837 110.733 29.9819 110.733 32.817C110.733 35.6521 108.435 37.9503 105.6 37.9503H121.733C124.568 37.9503 126.867 40.2486 126.867 43.0837C126.867 45.9187 124.568 48.217 121.733 48.217H114.272C110.698 48.217 107.8 50.5153 107.8 53.3503C107.8 55.2404 109.267 56.9515 112.2 58.4837C115.035 58.4837 117.333 60.7819 117.333 63.617C117.333 66.4521 115.035 68.7503 112.2 68.7503H51.3333C48.4982 68.7503 46.2 66.4521 46.2 63.617C46.2 60.7819 48.4982 58.4837 51.3333 58.4837H22.7333C19.8982 58.4837 17.6 56.1854 17.6 53.3503C17.6 50.5153 19.8982 48.217 22.7333 48.217H52.0666C54.9017 48.217 57.2 45.9187 57.2 43.0837C57.2 40.2486 54.9017 37.9503 52.0666 37.9503H33.7333C30.8982 37.9503 28.6 35.6521 28.6 32.817C28.6 29.9819 30.8982 27.6837 33.7333 27.6837H63.0666C60.2316 27.6837 57.9333 25.3854 57.9333 22.5503C57.9333 19.7153 60.2316 17.417 63.0666 17.417H134.933ZM134.933 37.9503C137.768 37.9503 140.067 40.2486 140.067 43.0837C140.067 45.9187 137.768 48.217 134.933 48.217C132.098 48.217 129.8 45.9187 129.8 43.0837C129.8 40.2486 132.098 37.9503 134.933 37.9503Z"
          fill="#DBDBFA"
        />
        <path
          fillRule="evenodd"
          clipRule="evenodd"
          d="M95.826 16.6834L102.647 66.4348L103.26 71.4261C103.458 73.034 102.314 74.4976 100.706 74.695L57.7621 79.9679C56.1542 80.1653 54.6906 79.0219 54.4932 77.4139L47.8816 23.5671C47.7829 22.7631 48.3546 22.0313 49.1586 21.9326C49.1637 21.932 49.1688 21.9313 49.1739 21.9307L52.7367 21.5311L95.826 16.6834ZM55.6176 21.208L58.9814 20.8306Z"
          fill="white"
        />
        <path
          d="M55.6176 21.208L58.9814 20.8306M95.826 16.6834L102.647 66.4348L103.26 71.4261C103.458 73.034 102.314 74.4976 100.706 74.695L57.7621 79.9679C56.1542 80.1653 54.6906 79.0219 54.4932 77.4139L47.8816 23.5671C47.7829 22.7631 48.3546 22.0313 49.1586 21.9326C49.1637 21.932 49.1688 21.9313 49.1739 21.9307L52.7367 21.5311L95.826 16.6834Z"
          stroke="#7E7BF6"
          strokeWidth="2.5"
        />
        <path
          fillRule="evenodd"
          clipRule="evenodd"
          d="M93.9695 19.8144L100.144 64.9025L100.699 69.4258C100.878 70.8831 99.8559 72.2077 98.416 72.3845L59.9585 77.1065C58.5185 77.2833 57.2062 76.2453 57.0272 74.7881L51.0506 26.112C50.9519 25.308 51.5236 24.5762 52.3276 24.4775L57.0851 23.8934"
          fill="#F0F0FF"
        />
        <path
          fillRule="evenodd"
          clipRule="evenodd"
          d="M97.701 7.33301H64.2927C63.7358 7.33301 63.2316 7.55873 62.8667 7.92368C62.5017 8.28862 62.276 8.79279 62.276 9.34967V65.083C62.276 65.6399 62.5017 66.1441 62.8667 66.509C63.2316 66.874 63.7358 67.0997 64.2927 67.0997H107.559C108.116 67.0997 108.62 66.874 108.985 66.509C109.35 66.1441 109.576 65.6399 109.576 65.083V19.202C109.576 18.6669 109.363 18.1537 108.985 17.7755L99.1265 7.92324C98.7484 7.54531 98.2356 7.33301 97.701 7.33301Z"
          fill="white"
          stroke="#7F7CFA"
          strokeWidth="2.5"
        />
        <path
          d="M98.026 8.17871V16.6833C98.026 17.8983 99.011 18.8833 100.226 18.8833H106.044"
          stroke="#807EFA"
          strokeWidth="2.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
        <path
          d="M70.1594 56.2838H89.2261M70.1594 18.8838H89.2261H70.1594ZM70.1594 27.6838H101.693H70.1594ZM70.1594 37.2171H101.693H70.1594ZM70.1594 46.7505H101.693H70.1594Z"
          stroke="#817FFA"
          strokeWidth="2.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        />
      </svg>
    );
```

## Making API Requests to Our Server
We need to make API requests to our server, Strapi provides `axiosInstance` to help with that let’s see how to make use of it.


1. Navigate to `src/plugins/slugify/admin/src/api`. 
2. Create a `slug.ts` file.
3. Update it’s content with the following lines of code:

```js
    import axiosInstance from '../../src/utils/axiosInstance';
    
    const slugRequests = {
    
      getContentTypes: async () => {
        const data = await axiosInstance.get(`/slugify/allContentTypes`);
        return data;
      },
    
      setSlugs: async (data: any) => {
        const res = await axiosInstance.post('/slugify/setSlugs', data)
        return res
      }
      
    }
    export default slugRequests;
```

We can now use this file to import data into our homepage and retrieve data from our server.


1. Head back to your `src/plugins/slugify/admin/src/pages/HomePage/index.tsx` file and update it’s contents with the following code:

```js
    import React, { Suspense, memo, useState, useEffect } from 'react';
    import slugRequests from '../../api/slugs';
    
    import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
    import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
    import { Illo } from '../../components/Illo';
    
    const HomePage: React.VoidFunctionComponent = () => {
    
      const [contentTypeCount, setContentTypeCount] = useState(0);
      const [ contentTypes, setContentTypes ] = useState(Array<any>)
      const [ disable, setDisabled ] = useState(false)
      const [ entries, setEntry ] = useState(contentTypes)
      const [ loading, setLoading ] = useState(false)
    
      return (
          <>
              <BaseHeaderLayout
                title="Slugify Plugin"
                subtitle="Click checkbox to allow slugs on Content Type"
                as="h2"
              />
      
              <ContentLayout>
                {contentTypeCount === 0 && !loading && (
                  <EmptyStateLayout icon={<Illo />} content="You don't have any Content Types yet..." />
                )}
              </ContentLayout>
          </>
        );
    };
    
    export default memo(HomePage);
```

## Using Custom-Made Strapi Components
We have already used certain Strapi components, e.g when we did `import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';` in the code snippet above. 

To style our client area with the basic Strapi theme, Strapi includes a couple of useful components. You can have a look at them to see which you’d like to use on the [Strapi design system.](https://design-system-git-main-strapijs.vercel.app/?path=/story/design-system-components-accordion--base)

Update the content of your `src/plugins/slugify/admin/src/pages/HomePage/index.tsx` file with the following code:

```js
    import React, { Suspense, memo, useState, useEffect } from 'react';
    import slugRequests from '../../api/slugs';
    import { Box } from '@strapi/design-system/Box';
    import { Flex } from '@strapi/design-system/Flex';
    import { Typography } from '@strapi/design-system/Typography';
    import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
    import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
    import { Checkbox } from '@strapi/design-system/Checkbox';
    import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
    import { Radio, RadioGroup } from '@strapi/design-system/Radio';
    import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/design-system/Table';
    import { Button } from '@strapi/design-system/Button';
    import { Icon } from '@strapi/design-system/Icon';
    import { LoadingIndicatorPage, CheckPagePermissions, useGuidedTour, useAutoReloadOverlayBlocker } from '@strapi/helper-plugin';
    import { Illo } from '../../components/Illo';
    const HomePage: React.VoidFunctionComponent = () => {
      
      const [contentTypeCount, setContentTypeCount] = useState(0);
      const [ contentTypes, setContentTypes ] = useState(Array<any>)
    
      const [ disable, setDisabled ] = useState(false)
      const [ entries, setEntry ] = useState(contentTypes)
      const [ loading, setLoading ] = useState(false)
    
      const ROW_COUNT = 6;
      const COL_COUNT = 10;
    
      useEffect(() => {
        fetchData()
      }, [setContentTypeCount]);
      function fetchData() {
        slugRequests.getContentTypes().then((res: any) => {
          setContentTypeCount(res.data.length);
          res.data.map((e: any) => {
            e.slugEnabled = !!e.pluginOptions.slug
            e.slugField = e.pluginOptions?.slug?.field
            e.savable = false
          })
          setContentTypes(res.data)
          setEntry(res.data)
        });
      };
      return (
        <>
            <BaseHeaderLayout
              title="Slugify Plugin"
              subtitle="Click checkbox to allow slugs on Content Type"
              as="h2"
            />
            <ContentLayout>
              {loading == true && (
                <LoadingIndicatorPage />)
              }
              {contentTypeCount === 0 && !loading && (
                <EmptyStateLayout icon={<Illo />} content="You don't have any Content Types yet..." />
              )}
              {contentTypeCount > 0 && !loading && (
                <Box background="neutral0" hasRadius={true} shadow="filterShadow">
                  <Flex justifyContent="space-between" padding={5}>
                    <Typography variant="alpha">You have a total of {contentTypeCount} contentTypes 🚀</Typography>
                  </Flex>
                  <Table colCount={COL_COUNT} rowCount={ROW_COUNT}>
                    <Thead>
                      <Tr>
                        <Th>
                          <BaseCheckbox checked={disable} onClick={() => setDisabled(!disable)} aria-label="Select all entries" />
                        </Th>
                        <Th>
                          <Typography variant="sigma">S/N</Typography>
                        </Th>
                        <Th>
                          <Typography variant="sigma">Collection Name</Typography>
                        </Th>
                        <Th>
                          <Typography variant="sigma">UID</Typography>
                        </Th>
                        <Th>
                          <Typography variant="sigma">Categories</Typography>
                        </Th>
                        <Th>
                          <Typography variant="sigma">Action</Typography>
                        </Th>
                      </Tr>
                    </Thead>
                    <Tbody>
                      {contentTypes.map((entry: any, i: number) => <Tr key={entry.collectionName}>
                          <Td>
                            <BaseCheckbox checked={entry.slugEnabled} key={i} onClick={(e:any) => {
                              entry.slugEnabled = !entry.slugEnabled
                              entry.savable = !entry.savable
                              setEntry((arr): any => {
                                return arr.map((el) => {
                                  if(el.collectionName == entry.collectionName) {
                                    return { ...el, slugEnabled: entry.slugEnabled }
                                  }
                                  else {
                                    return { ...el, slugEnabled: el.slugEnabled }
                                  }
                                })
                              })
                            }} />
                          </Td>
                          <Td>
                            <Typography textColor="neutral800">{i+1}</Typography>
                          </Td>
                          <Td>
                            <Typography textColor="neutral800">{entry.collectionName}</Typography>
                          </Td>
                          <Td>
                            <Typography textColor="neutral800">{entry.uid}</Typography>
                          </Td>
                          <Td> 
                            <Flex>
                              <RadioGroup labelledBy={`contentType-${i}`} onChange={(e: any) => {
                                  entry.slugField = e.target.value
                                  if(entry.slugEnabled) {
                                    entry.savable = true
                                  }
                                  e.stopPropagation()
                                  setEntry((arr): any => {
                                    return arr.map((el) => {
                                      if(el.collectionName == entry.collectionName) {
                                        return { ...el, slugField: e.target.value }
                                      }
                                      else {
                                        return { ...el, slugField: el.slugField }
                                      }
                                    })
                                  })
                                }} name={`contentType-${i}`}>
                                {
                                  Object.keys(entry.attributes).map((attr: any, e: number) => {
                                    if(entry.attributes[attr].type == 'string' && attr !== 'slug' ) return <Radio padding={3} key={e} checked={ entry.slugField == attr } value={attr}>{attr}</Radio>
                                })
                                }
                              </RadioGroup>
                            </Flex>
                          </Td>
                          <Td>
                            <Typography textColor="neutral800">
                              <Button disabled={ !entry.savable } key={i} onClick={
                                async () => {
                                  if(entry.slugEnabled && !entry.slugField) return
                                  slugRequests.setSlugs(entry)
    
                                  setLoading(true)
                                  // Make sure the server has restarted
                                  await slugRequests.serverRestartWatcher(true);
                                  setLoading(false)
                                  fetchData()
                                }
                              } startIcon={<Icon/>}>
                                Save
                              </Button>
                            </Typography>
                          </Td>
                        </Tr>)}
                    </Tbody>
                  </Table>
                </Box>
              )}
            </ContentLayout>
        </>
      );
    };
    export default memo(HomePage);
```
In the code snippet above, a couple of things are going on; let’s try to understand exactly what’s going on:


1. Using the `Table` component from the `strapi design system`, we display our `content-types`.
2. Then, we’re using both the `Radio` and `Select` components to allow user choose what `content-type` should use our plugin.
3. We are also using the inbuilt `Loader` in order to let users know when our app is in a loading state.


## Listening for Server Restarts

This line of code `slugRequests.serverRestartWatcher(true)` helps to solve a particular problem.

**Problem**: 
Everytime we save an entry (i.e enable or disble our plugin for a certain `content-type`), the server restarts. We want to enable a loading state as seen below, but at what point do we disable the loading state. 

```js
      <Button disabled={ !entry.savable } key={i} onClick={
          async () => {
            if(entry.slugEnabled && !entry.slugField) return
            slugRequests.setSlugs(entry)
    
            setLoading(true)
            // Make sure the server has restarted
            await slugRequests.serverRestartWatcher(true);
            setLoading(false)
            fetchData()
          }
        } startIcon={<Icon/>}>
          Save
        </Button>
```

**Possible solution**: 
One way we could overcome the problem is by setting up a timer such as `setTimeout()`, therefore causing the loading state to be disabled after a couple of seconds then refetch the data from our backend. However, this approach has a few drawbacks. Since we are estimating the number of seconds it will take for our server to restart:
1. Our `fetchData()` function might execute before the server entirely restarts, rendering our data null. Where we have several jobs running on our CPU, the server recovers slowly.
2. We keep users waiting in a case where the server restarts sooner than we anticipated.

**Preferred solution**: 
A better method could be to send a request to the server to check if it’s still alive, and we keep sending the requests until we get a valid response.

Update the content of your `src/plugins/slugify/admin/src/api/slug.ts` file with the code below.

```js
    import axiosInstance from '../../src/utils/axiosInstance';
    const SERVER_HAS_NOT_BEEN_KILLED_MESSAGE = 'did-not-kill-server';
    const SERVER_HAS_BEEN_KILLED_MESSAGE = 'server is down';
    const slugRequests = {
      getContentTypes: async () => {
        const data = await axiosInstance.get(`/slugify/allContentTypes`);
        return data;
      },
      setSlugs: async (data: any) => {
        const res = await axiosInstance.post('/slugify/setSlugs', data)
        return res
      },
      serverRestartWatcher: async(response: any, didShutDownServer: boolean=false) => {
          return new Promise(resolve => {
            fetch(`${process.env.STRAPI_ADMIN_BACKEND_URL}/_health`, {
              method: 'HEAD',
              mode: 'no-cors',
              headers: {
                'Content-Type': 'application/json',
                'Keep-Alive': 'false',
              },
            })
              .then(res => {
                if (res.status >= 400) {
                  throw new Error(SERVER_HAS_BEEN_KILLED_MESSAGE);
                }
        
                if (!didShutDownServer) {
                  throw new Error(SERVER_HAS_NOT_BEEN_KILLED_MESSAGE);
                }
        
                resolve(response);
              })
              .catch(err => {
                setTimeout(() => {
                  return slugRequests.serverRestartWatcher(
                    response,
                    err.message !== SERVER_HAS_NOT_BEEN_KILLED_MESSAGE
                  ).then(resolve);
                }, 100);
              });
          });
        }
    }
    export default slugRequests;
```

This is the method that Strapi uses to detect server restarts, and then act accordingly.


## Conclusion
 I surely do hope that you have the skillset to set out and create your own plugins; you could even get it listed in the [Strapi marketplace](https://market.strapi.io/). The [Strapi marketplace](https://market.strapi.io/) has a collection of plugins that could help boost your development process; feel free to check it out. [Click here to access the Github repo for this project.](https://github.com/oviecodes/strapi-slugs).