Strapi V4 search by slug instead ID

Greetings,

I have a question regarding findOne method. I would like to search by SLUG instead default ID. Do you have any ideas on how to implement this scenario? I have searched the docs, but there is a manual for V3 of Strapi.

Thanks for your answers. This topic is quite crucial and many users will find it usefull.

7 Likes

I agree, this would be very helpful to a lot of users. I understand that v4 is really new and writing new documentation takes time, but I think it’s a bit premature making v4 the default and not having all the docs fully ready.

3 Likes

Hi,

Updating because I just found it:

http://localhost:1337/api/your-content-type-plural?filters[your-slug-field-name][operator]=your-slug-goes-here
# In my case:
http://localhost:1337/api/posts?filters[slug][$eq]=entrada-de-prueba-2

Source: REST API - Strapi Developer Docs

4 Likes

@Myddna this works but you are using find() method, not findOne() and they behave differently.

For example if post with slug does not exists you still get 200 response with this approach, while with findOne (when providing id) you would get 404.

1 Like

You will get a 200 res, but no data… you can just validate if data is present on the front-end. I’d also use the same method for findOne, even if the id is ok but no content is returned I would treat it as a 404.

You can override this controller action in order to do so.
At least in version 3.6 which I have used widely so it worked.
To do so you must go in api/articles/controllers/articles.js

Check below link, maybe it still works same (similar)

I did try to modify the example to override controller but with no success.

I don’t know how to pass slug to await strapi.service('api::restaurant.restaurant').findOne(id, query); ?

Working now for me after following the docs Myddna linked and querying for /api/your-content-type?filters[slug][operator]=your-slug-string. You do recieve a status 200 though but for me using Next.js with getStaticPaths I manage to recieve 404 responses for pages not defined in the paths.

1 Like

Thank you for this!!! Everything on the Internet is showing Strapi 3 examples.

DOES NOT WORK: http://localhost:1337/posts?slug=${slug}

WORKS: http://localhost:1337/api/posts?filters[slug][$eq]=S-L-U-G

@Strapy you need to update documentation and examples for basic things ASAP.

1 Like

Hi, you can redefine your findOne method in your controller.
You cannot find by id when did this change
That is an example of your redefined findOneMethod

module.exports = createCoreController('api::product.product', ({ strapi }) =>  ({
  async findOne(ctx) {
    const { id: slug } = ctx.params;
    const { query } = ctx;
    if (!query.filters) query.filters = {}
    query.filters.slug = { '$eq': slug }
    const entity = await strapi.service('api::product.product').find(query);
    const { results } = await this.sanitizeOutput(entity, ctx);

    return this.transformResponse(results[0]);
  }
}))

Other option is to create a custom route like /api/products/product/:slug and use it instead of override the strapi one, you mantain twice with this option

6 Likes

Could you share an example of how to do it in GraphQL as well?

my colection type have name Project then use getProjects and Projects

query getProjects($slug: String!) {
projects(filters: { slug: { eq: $slug } } ) {
data {
id,
attributes {
name…

2 Likes

thanks! the filters way works
i had the same problem

I understand and appreciate everyone’s solutions here. Making a find query and then just taking the first result seems a bit of a backwards step (e.g. v3 → v4). It’s a bit annoying how v4 has sort of been thrown out as a sink or swim release.

I also don’t think it’s great that if you do want to do this with a findOne function, you need to go down to the database layer. The point of a headless CMS and this kind of abstraction is that we shouldn’t need to go to the lower level for something as trivial as a findOne query

This was probably helpful, it still doesn’t work for me. Using smart query on Nuxt.

I only get the id examples to work, I can’t seem to get anything to work beyond that. This is what I tried based on your suggestion. Any idea?

component.vue

    apollo: {
    article: {
      prefetch: true,
      query: articleQuery,
       variables() {
                 return { slug: $route.params.slug };
      },

article.gql

query Articles($slug: String!) {
  articles(filters:{slug: {eq: $slug}}) {
    data {
      id
      attributes {
        slug
        title...

As the URL is set to consume, I do not understand what they go inside the variables, thanks

Thanks a lot!

I was able to solve it with this code.

module.exports = createCoreController('api::blog.blog', ({ strapi }) => ({

    // Method 3: Replacing a core action
    async findOne(ctx) {
        const { id } = ctx.params;
        const entity = await strapi.db.query('api::blog.blog').findOne({
            where: { slug: id }
        });
        const sanitizedEntity = await this.sanitizeOutput(entity, ctx);

        return this.transformResponse(sanitizedEntity);
    }
}));
3 Likes

Thanks @SHUICHI_MINAMIE this worked for me どうも :nerd_face:

1 Like

I was playing around with this, here is another way that worked for me.

GitHub Link Example

1 Override existing route to use slug vs id

src/api/post/routes/post.js

"use strict";

/**
 * post router.
 */

const { createCoreRouter } = require("@strapi/strapi").factories;
const defaultRouter = createCoreRouter("api::post.post");

// function to add to or override default router methods
const customRouter = (innerRouter, routeOveride = [], extraRoutes = []) => {
  let routes;

  return {
    get prefix() {
      return innerRouter.prefix;
    },
    get routes() {
      if (!routes) routes = innerRouter.routes;

      const newRoutes = routes.map((route) => {
        let found = false;

        routeOveride.forEach((overide) => {
          if (
            route.handler === overide.handler &&
            route.method === overide.method
          ) {
            found = overide;
          }
        });

        return found || route;
      });

      return newRoutes.concat(extraRoutes);
    },
  };
};

// Overide the default router with the custom router to use slug.
const myOverideRoutes = [
  {
    method: "GET",
    path: "/posts/:slug",
    handler: "api::post.post.findOne",
  },
];

const myExtraRoutes = [];

module.exports = customRouter(defaultRouter, myOverideRoutes, myExtraRoutes);


2 Override existing controller to use slug instead of id

src/api/post/controllers/post.js

"use strict";

/**
 *  post controller
 */

const { createCoreController } = require("@strapi/strapi").factories;

module.exports = createCoreController("api::post.post", ({ strapi }) => ({
  async findOne(ctx) {
    const { slug } = ctx.params;

    const query = {
      filters: { slug },
      ...ctx.query,
    };

    const post = await strapi.entityService.findMany("api::post.post", query);

    const sanitizedEntity = await this.sanitizeOutput(post);

    return this.transformResponse(sanitizedEntity[0]);
  },
}));

query

http://localhost:1337/api/posts/how-to-use-a-slug-vs-an-id-field

response

{
"data": {
"id": 1,
"attributes": {
"title": "How to use a slug vs an id field",
"content": "## My content",
"slug": "how-to-use-a-slug-vs-an-id-field",
"createdAt": "2022-06-16T21:20:05.209Z",
"updatedAt": "2022-06-16T23:32:45.288Z",
"publishedAt": "2022-06-16T21:20:08.070Z"
}
},
"meta": {}
}

query

http://localhost:1337/api/posts/how-to-use-a-slug-vs-an-id-field?populate=*

response

{
"data": {
"id": 1,
"attributes": {
"title": "How to use a slug vs an id field",
"content": "## My content",
"slug": "how-to-use-a-slug-vs-an-id-field",
"createdAt": "2022-06-16T21:20:05.209Z",
"updatedAt": "2022-06-16T23:32:45.288Z",
"publishedAt": "2022-06-16T21:20:08.070Z",
"image": {
"data": {
"id": 1,
"attributes": {
"name": "cat2.jpg",
"alternativeText": "cat2.jpg",
"caption": "cat2.jpg",
"width": 2392,
"height": 2500,
"formats": {
"thumbnail": {
"name": "thumbnail_cat2.jpg",
"hash": "thumbnail_cat2_bf2dde0b7d",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 150,
"height": 156,
"size": 4.66,
"url": "/uploads/thumbnail_cat2_bf2dde0b7d.jpg"
},
"large": {
"name": "large_cat2.jpg",
"hash": "large_cat2_bf2dde0b7d",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 957,
"height": 1000,
"size": 120.28,
"url": "/uploads/large_cat2_bf2dde0b7d.jpg"
},
"medium": {
"name": "medium_cat2.jpg",
"hash": "medium_cat2_bf2dde0b7d",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 718,
"height": 750,
"size": 70.24,
"url": "/uploads/medium_cat2_bf2dde0b7d.jpg"
},
"small": {
"name": "small_cat2.jpg",
"hash": "small_cat2_bf2dde0b7d",
"ext": ".jpg",
"mime": "image/jpeg",
"path": null,
"width": 478,
"height": 500,
"size": 32.39,
"url": "/uploads/small_cat2_bf2dde0b7d.jpg"
}
},
"hash": "cat2_bf2dde0b7d",
"ext": ".jpg",
"mime": "image/jpeg",
"size": 544.61,
"url": "/uploads/cat2_bf2dde0b7d.jpg",
"previewUrl": null,
"provider": "local",
"provider_metadata": null,
"createdAt": "2022-06-16T23:32:42.135Z",
"updatedAt": "2022-06-16T23:32:42.135Z"
}
}
}
}
},
"meta": {}
}
1 Like