deliver images via strapi custom route and koa.js context object

deliver images via strapi custom route and koa.js context object

In my strapi powered band website journey I created a Song collection type to store mp3 files and some of their meta data (title, artist, duration...). Most of the mp3s include a cover image, a photo taken during the rehearsal, overhauled with gimp and I want to access these images to show them on the websites mp3 player.

In this post I show how to create a custom route on the Song collection, parsing the image data out of an mp3 file and return the image to the browser.

A working code example of this blog post is available at github: https://github.com/djpogo/strapi-node-upload/tree/custom-route.

adding a custom route to my Song collection

My idea is, to add a custom route to the song collection, so I can access a mp3 cover image by calling http://localhost:1337/songs/1/cover. To achieve this goal my starting point is reading strapis documentation about routing.

Now I extend my api/song/config/routes.json with a new path /songs/:id/cover pointing to a song.cover handler:

// api/song/config/routes.json
{
  "routes": [
    // ... song routes provided by strapi
    {
      "method": "GET",
      "path": "/songs/:id",
      "handler": "song.findOne",
      "config": {
        "policies": []
      }
    },
    // ↓ this is the new added route ↓
    {
      "method": "GET",
      "path": "/songs/:id/cover",
      "handler": "song.cover",
      "config": {
        "policies": []
      }
    },
    // ... more song routes provided by strapi
  ]
}

strapi controller and koa.js

At the end of the routing documentation they give a hint to extend the controller of your collection. Another look at the controller documentation and I know where to go next.

I open the file api/song/controllers/song.js and it looks like this:

// api/song/controllers/song.js
'use strict';

/**
 * Read the documentation (https://strapi.io/documentation/v3.x/concepts/controllers.html#core-controllers)
 * to customize this controller
 */

module.exports = {};

In this use case I leave the strapi environment and find out that strapi itself is build on the koa.js framework and that the ctx param is the full koa.js context object, so I have the full control to do what I want within a route.

custom route code

I will add a cover function to api/song/controller/song.js, printing "Hello World" as response. Make sure to name the function same as the handler name in routes.json.

// api/song/controller/song.js
'use strict';

module.exports = {
  cover: async (ctx) => {
    return 'Hello World';
  }
};

custom route permission

I always start with a print Hello World, to see if I am in the right place of code. If this code produces the expected output on http://localhost:1337/songs/1/cover add more logic, but right now it tells me:

insomnia strapi / custom route responds with 403

As strapi user a 403 response is never bad because it tells you two things:
* I (strapi) know this route
* the client you're using is (right now) not allowed to consume this route
Lets visit the role & permissions dialog and allow everyone to access this route. You may want to allow only authenticated user this route in future, but for now everyone is fine.

A 404 strapi response tells us, that strapi does not know this route or no data is available - in this example a 404 response would tell us to fix routing.json or handler code.
strapi / roles & permissions / public user / allow cover route

After allowing everyone access the cover route, reload http://localhost:1337/songs/1/cover and it produces the expected output:

insomnia strapi / custom route reponds with 200 and "Hello World"

back to route code

The purpose of this route is to responds directly with the mp3 cover or a fallback image. The steps for this route are:
* check if song entry exists
* parse mp3 file (utilizing music-metadata)
* check for image data in the mp3 file
* load fallback image if Song entity not exists or mp3 file does not have a cover image
* respond with a directly usable image, you can use as img src or background-image: url().

#1 check song entry existence

// api/song/controllers/song.js
module.exports = {
  cover: async (ctx) => {
    const { id } = ctx.params;
    const songData = await strapi.services.song.findOne({ id });
    // tbc
  },
};  

In this first step I stay in strapi land using strapi functions to retrieve the collection entry.
After the strapi service call songData either is a Song object or null.

#2 parse mp3 file

// api/song/controllers/song.js

const fs = require('fs');
const mm = require('music-metadata');

module.exports = {
  cover: async (ctx) => {
    const { id } = ctx.params;
    const songData = await strapi.services.song.findOne({ id });
    const image = {};
    try {
      const song = await mm.parseFile(`./public${songData.mp3.url}`, { duration: false, skipCovers: false });
      image.image = song.common.picture[0].data;
      image.type = song.common.picture[0].format;
    } catch (err) {
      image.image = fs.readFileSync('./public/oleg-ivanov-G_3NA_UoVyo-unsplash.jpg');
      image.type = 'image/jpeg';
    }

In this step the mp3 file is parsed. If an error occures it falls back loading a default image.

For production use, caching mp3 cover images is strongly recommended, because it is a time/cpu consuming task.

As fallback image I use this image from oleg ivanov, put in the public folder, but you can put that file anywhere in your strapi project, where the node process can read it.

This code produces always an image object with image data and type. This is pure nodejs code, so let's see what koa.js needs to do now.

#3 sending image to browser using koa.js context

// api/song/controller/song.js
    ...
    ctx.response.set('content-type', image.type);
    ctx.response.body = image.image;
  }
};

And that's it.

insomnia strapi / custom cover route on existing mp3 (blurring done by me)

If an entity does not exist or the mp3 file has no image data:

Image © by Oleg Ivanov @ unsplash

conclusion

Strapi not only provides data modelling, data api-access and api securing, it also gives you the full control and flexibility what you want to do within a route.
I hope my example may be a starting point for your use case to use strapi in your way.
Check out the example repository of this blog post: https://github.com/djpogo/strapi-node-upload/tree/custom-route. Make sure to look at the custom-route branch, the main branch contains code for this blog post.

Happy coding.

Article Image from Jan Huber via unsplash and ghost ♥.