Creating a Searchable Reading List with Strapi CMS Custom API

Uncover the efficient way to create a searchable reading list using Strapi CMS in this tutorial. Ideal for programmers who want to efficiently manage and access their valuable learning resources, this guide simplifies the process, enhancing your programming lifestyle with organized and easily retrievable information.

Cover post
Author avatar

S. Amir Mohammad Najafi

September 19, 2023

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.


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!


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

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

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

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


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.


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};

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


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};


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: [],

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


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 ♥️

Improve this page Keywords: strapi, idea