Replace findOne Default ID with a Custom UID in Strapi v4

Replace findOne Default ID with a Custom UID in Strapi v4

When you create a new entry in a Strapi collection type, a default ID is assigned to that record. And when you query the collection's endpoint to fetch a single record, you'll have to use the default ID. So your query looks something like this - localhost:1337/api/blogs/1

But this isn't always the experience we need. With a blog, for example, your frontend code might be set up to fetch the blog post based on the ID in the URL e.g. yoursitename.com/blog/:id.

And yoursitename.com/blog/the-future-of-web-dev.. is much better than yoursitename.com/blog/3. This is because a slug is better for readability, sharing, user experience, and SEO.

This short blog post will cover how to change the ID used in a findOne single-record search to a slug or any other custom UID (unique identifier). I'm assuming that you already have Strapi set up, along with your Collection Type. If not, see this guide from Strapi.

Setting Up

If you haven't already, you need to set up the custom ID to be used in place of the default ID. Strapi offers a UID field in the Content-Type Builder. In this example, I'll use a Blog collection type and set up a slug UID. The steps should be the same regardless of your content type and UID.

  1. If you haven't yet, ensure the findOne method is enabled for the Collection Type. Navigate to Settings > Roles > Public.

  2. Under Permissions, expand your Collection Type and select findOne. Ensure you click 'Save' at the top of the page to save your changes.

  3. Next, navigate to Content Type Builder > Select the Collection Type.

  4. Select 'Add another field to this collection type.'

  5. On the modal that pops up, select UID.

  6. Under basic settings, enter a name. I'll use 'slug.' Then navigate to advanced settings and check the 'Required field' option. You can customize this field further if needed, then click 'Finish.'

  7. Remember to click the 'Save' button at the top of the Content-Type Builder page to ensure your new field has been saved. You should now have this in your collection.

Go ahead and update the entries in your collection with slugs (or your UID type). Or create new entries with that UID.

Updating the code

Next, we will update our controller logic to replace the default ID with a slug in findOne searches.

  1. In your IDE, with the Strapi project open, navigate to src > api > [collection_type] > controllers > [collection_type.ts] . In my case collection type is 'blog' so my path will look like src > api > blog > controllers > blog.ts

  2. Replace the default factories.createCoreController() code there with the one below. I'll explain it line by line.

     export default factories.createCoreController(
       "api::blog.blog",
       ({ strapi }) => ({
         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);
         },
       })
     );
    

    This code exports a Strapi controller with a findOne method. When this method is called, it retrieves a single blog post based on the slug provided in the request context, sanitizes the output, transforms it for the response, and then returns it.

    Here's a breakdown of the code:

    1. export default factories.createCoreController("api::blog.blog", ({ strapi }) => : This line exports a controller created for the blog model (api::blog.blog). The object within the function takes in a destructured argument strapi, the Strapi instance.

    2. async findOne(ctx): This function is an asynchronous method called findOne. This function is a controller method to find a single record based on the context ctx.

    3. const { id } = ctx.params: This line extracts the id from the parameters provided in the request context (ctx).

    4. const entity = await strapi.db.query("api::blog.blog").findOne({ where: { slug: id } }): This sends a query to the database to find one blog post where the slug matches the id extracted from the ctx.params. The found blog post is stored in entity.

    5. const sanitizedEntity = await this.sanitizeOutput(entity, ctx): The sanitizeOutput function is used to cleanse or format the blog post data before sending it as a response. This is a good practice as it ensures that the data adheres to a certain format and prevents any sensitive data from being accidentally exposed.

    6. return this.transformResponse(sanitizedEntity): This line transforms the sanitized blog post data into a suitable format for response. The transformed data is then returned as the result of the findOne method.

  3. Restart the server after making these changes.

  4. Once your server has restarted, test the endpoint using a slug from one of your collection entries. Mine looks like this -localhost:1337/api/blogs/what-is-productivity-and-why-does-it-matter-1

Following these steps, you can fetch a single record using its custom UID.

Please let me know if the steps in this blog post help you or if you have any questions. Happy coding!