DEV Community

S. Amir Mohammad Najafi
S. Amir Mohammad Najafi

Posted on • Originally published at njfamirm.ir on

Creating a Searchable Reading List with Strapi CMS Custom API

Hi everyone!

As programmers, we are constantly learning and reading articles, blog posts, and documentation. Maybe this is the most important part of programming lifestyle!

Unfortunately, we do not keep those resource anywhere, and even if we keep them on our notes because it is not accessible through searching, we can't access them anymore.

Idea

An interesting solution for our problem is a searchable reading list that's I got this idea from the esif.dev blog.

At the end, you can setup and maintain a searchable-reading-list of valuable resources that you encounter during your programming journey!

Roadmap

Since we may read several articles in a day and need to put it in the reading-list, We needed to have a CMS, so that I could put the links there very quickly and easily and not think about anything else.

Also, our reading list page should work with the API and not need to be built statically like other pages with Eleventy. So I decided to use Strapi as a beautiful, dynamic, and easy-to-use CMS.

Preparing the environment

Before we can start building our reading-list, we need to create a new Strapi project.

yarn create strapi-app reading-list --ts
Enter fullscreen mode Exit fullscreen mode

Create Content Type

First, we need to create a content-type that we can put our reading-list in, So to create a content type for our reading-list, we can use the Strapi CLI and run the following command in our project directory. This will generate a new content type with the specified name and fields.

This command will create a new content type called reading-list and generate the necessary files and fields to get started building our custom API.

yarn strapi generate

  ? Strapi Generators content-type - Generate a content type for an API
  ? Content type display name Reading List
  ? Content type singular name reading-list
  ? Content type plural name reading-lists
  ? Please choose the model type Collection Type
  ? Use draft and publish? No
  ? Do you want to add attributes? Yes
  ? Name of attribute title
  ? What type of attribute string
  ? Do you want to add another attribute? Yes
  ? Name of attribute url
  ? What type of attribute string
  ? Do you want to add another attribute? Yes
  ? Name of attribute keyword
  ? What type of attribute text
  ? Do you want to add another attribute? Yes
  ? Name of attribute description
  ? What type of attribute richtext
  ? Do you want to add another attribute? Yes
  ? Name of attribute keyword
  ? What type of attribute text
  ? 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? reading-list
  ? Bootstrap API related files? Yes
  ✔ ++ /api/reading-list/content-types/reading-list/schema.json
  ✔ +- /api/reading-list/content-types/reading-list/schema.json
  ✔ ++ /api/reading-list/controllers/reading-list.ts
  ✔ ++ /api/reading-list/services/reading-list.ts
  ✔ ++ /api/reading-list/routes/reading-list.ts
Enter fullscreen mode Exit fullscreen mode

This is a base schema, but we can improve them by adding regex and unique to the url field and also making the title and the url required, this is result of the schema can put under api/reading-list/content-types/reading-list/schema.json:

{
  "kind": "collectionType",
  "collectionName": "reading_lists",
  "info": {
    "singularName": "reading-list",
    "pluralName": "reading-lists",
    "displayName": "Reading List",
    "description": ""
  },
  "options": {
    "draftAndPublish": false
  },
  "attributes": {
    "title": {
      "type": "string",
      "required": true
    },
    "url": {
      "type": "string",
      "regex": "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)",
      "required": true,
      "unique": true
    },
    "description": {
      "type": "richtext"
    },
    "keyword": {
      "type": "text",
      "required": false
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Screenshot of strapi content type

Very good, Now we have simple reading list in strapi, let's make them searchable!

Search Custom API

In this step we'll use the strapi-generate command again to creates a custom API pure file structure.

yarn strapi generate

  ? Strapi Generators api - Generate a basic API
  ? API name reading-list-search
  ? Is this API for a plugin? No
  ✔ ++ /api/reading-list-search/routes/reading-list-search.ts
  ✔ ++ /api/reading-list-search/controllers/reading-list-search.ts
  ✔ ++ /api/reading-list-search/services/reading-list-search.ts
Enter fullscreen mode Exit fullscreen mode

Now we need to fill the files with the correct codes.

Service

Services are a set of reusable functions to simplify controllers' logic. ReadMore

In this step, we use the service to create a function to get the reading list and filter them. The keyword parameter is used to filter the reading-list based on their keywords, and if no keyword is given, it returns all content of reading-list.

api/reading-list-search/services/reading-list-search.js:

import {errors} from '@strapi/utils';

async function search(keyword?: string) {
  try {
    const entries = await strapi.entityService.findMany('api::reading-list.reading-list', {});

    let filteredEntries = entries;
    if (keyword) {
      filteredEntries = entries.filter((entry) => {
        const keywords = entry.keyword.split('\n').map((keyword) => keyword.trim());
        return keywords.includes(keyword);
      });
    }

    return filteredEntries;
  } catch (err) {
    throw new errors.ApplicationError('Something went wrong');
  }
}

export default {search};
Enter fullscreen mode Exit fullscreen mode

this function first gets reading-list with findMany and then filters them using the typescript filter function.

Controller

Controllers contain a set of methods, called actions, reached by the client according to the requested route. ReadMore

To write a controller for the reading-list search function, set the content of the api/reading-list-search/controllers/reading-list-search.js file something like this to pass keyword search query into search service function.

async function search(ctx) {
  try {
    return await strapi.service('api::reading-list-search.reading-list-search').search(ctx.request.query.keyword);
  } catch (err) {
    ctx.badRequest('Reading list search controller error', {detail: err});
  }
}

export default {search};
Enter fullscreen mode Exit fullscreen mode

Route

Requests sent to Strapi on any URL are handled by routes. ReadMore

We must add this endpoint and sets the previous controller as the route handler, put this code to api/reading-list-search/routes/reading-list-search.ts:

export default {
  routes: [
    {
      method: 'GET',
      path: '/reading-list-search',
      handler: 'reading-list-search.search',
      config: {
        policies: [],
      },
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Now this route is accessible at /api/reading-list-search, of course if we do the next step 🤓!

Set Permission

By default, this API can't be accessible publicly so set the correct permission.

Screenshot of set permission for strapi custom API

Result

Congratulations 🎉, we create a custom API for searching in our reading-list using Strapi CMS.

Screenshot of reading list custom API response

I hope this article is useful for you, and it is a step towards a society where we grow together ♥️


Thanks @derrickmehaffy for improve this article.

Top comments (0)